This commit is contained in:
anlicheng 2025-06-20 16:02:44 +08:00
parent f8fd7d0c43
commit 92ea32568b
7 changed files with 327 additions and 41 deletions

View File

@ -3,6 +3,9 @@ modbus
An OTP application An OTP application
## 编译serial可执行文件
gcc serial.c -o serial
Build Build
----- -----

View File

@ -9,11 +9,11 @@
-author("anlicheng"). -author("anlicheng").
-record(modbus_transport_rtu, { -record(modbus_transport_rtu, {
port :: string(), port :: binary(),
baudrate :: integer(), baudrate :: integer(),
parity :: any(), parity = 0 :: integer(), %% 0: none; 1: odd; 2: even
stopbits :: integer(), stopbits :: integer(),
timeout = 0 :: integer() timeout = 0 :: integer() %% : 1s, 100ms等格式全部转换成毫秒
}). }).
-record(modbus_transport_tcp, { -record(modbus_transport_tcp, {
@ -45,11 +45,18 @@
retry_timeout :: integer(), retry_timeout :: integer(),
%% %%
variables = #{} :: map(), metrics = #{} :: map(),
controls = #{} :: map() controls = #{} :: map()
}). }).
-record(modbus_metric, {
name,
address,
type,
unit
}).
-record(modbus_processor, { -record(modbus_processor, {
name :: string(), name :: string(),
input :: string(), input :: string(),

View 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
%%%===================================================================

View 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.

View File

@ -54,8 +54,13 @@ parse(Input) when is_binary(Input) ->
end end
end, Trees), end, Trees),
AST = #ast{modbus = Modbus, devices = Devices, processors = Processors, alarms = Alarms }, case length(Modbus) == 1 of
{ok, AST} true ->
AST = #ast{modbus = hd(Modbus), devices = Devices, processors = Processors, alarms = Alarms },
{ok, AST};
false ->
{error, modbus_block_error}
end
catch throw:Reason -> catch throw:Reason ->
{error, Reason} {error, Reason}
end. end.
@ -163,7 +168,7 @@ parse_ast0(#block{ident = <<"device", Name0/binary>>, props = Props}) ->
poll_interval = maps:get(<<"poll_interval">>, MapProps, undefined), poll_interval = maps:get(<<"poll_interval">>, MapProps, undefined),
retries = map_of_integer(<<"retries">>, MapProps, 0), retries = map_of_integer(<<"retries">>, MapProps, 0),
retry_timeout = maps:get(<<"retry_timeout">>, MapProps, undefined), 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) controls = maps:get(<<"controls">>, MapProps, undefined)
}; };
parse_ast0(#block{ident = <<"processor", Name0/binary>>, props = Props}) -> 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(T, [{<<"recovery_actions">>, Props}|Acc]);
parse_ast1([#block{ident = <<"transform">>, props = Props}|T], Acc) -> parse_ast1([#block{ident = <<"transform">>, props = Props}|T], Acc) ->
parse_ast1(T, [{<<"transform">>, Props}|Acc]); parse_ast1(T, [{<<"transform">>, Props}|Acc]);
parse_ast1([#block{ident = <<"variables">>, props = Props}|T], Acc) -> parse_ast1([#block{ident = <<"metrics">>, props = Metrics0}|T], Acc) ->
parse_ast1(T, [{<<"variables">>, Props}|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([#block{ident = <<"controls">>, props = Props}|T], Acc) ->
parse_ast1(T, [{<<"controls">>, Props}|Acc]); parse_ast1(T, [{<<"controls">>, Props}|Acc]);
parse_ast1([#block{ident = <<"transport", Name0/binary>>, props = Props}|T], 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 -> validate([#modbus_device{slave_id = Id}|_]) when Id < 1 orelse Id > 247 ->
throw({invalid_slave_id, Id}); throw({invalid_slave_id, Id});
validate([#modbus_device{variables = _Vars}|T]) -> validate([#modbus_device{metrics = _Vars}|T]) ->
validate(T); validate(T);
validate([_|T]) -> validate([_|T]) ->
validate(T). validate(T).
@ -287,4 +306,10 @@ map_of_time(Key, M, Def) ->
end; end;
false -> false ->
Def Def
end. 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, "'").

View File

@ -12,6 +12,9 @@
-behaviour(gen_server). -behaviour(gen_server).
%% rtu指令
-define(CONNECT, 16#01).
%% API %% API
-export([start_link/1]). -export([start_link/1]).
-export([test/0]). -export([test/0]).
@ -22,7 +25,9 @@
-define(SERVER, ?MODULE). -define(SERVER, ?MODULE).
-record(state, { -record(state, {
port,
access_log_pid :: pid() | undefined,
error_log_pid :: pid() | undefined
}). }).
test() -> test() ->
@ -50,11 +55,19 @@ start_link(AST) ->
-spec(init(Args :: term()) -> -spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore). {stop, Reason :: term()} | ignore).
init([AST = #ast{modbus = Modbus}]) -> init([AST = #ast{modbus = Modbus, devices = Devices}]) ->
lager:debug("modbus is: ~p", [Modbus]), 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 %% @private
%% @doc Handling call messages %% @doc Handling call messages
@ -84,6 +97,10 @@ handle_cast(_Request, State = #state{}) ->
{noreply, NewState :: #state{}} | {noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}). {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{}) -> handle_info(_Info, State = #state{}) ->
{noreply, State}. {noreply, State}.
@ -109,9 +126,17 @@ code_change(_OldVsn, State = #state{}, _Extra) ->
%%% Internal functions %%% 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; 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 Timeout = case is_integer(Timeout0) andalso Timeout0 > 0 of
true -> true ->
Timeout0; Timeout0;
@ -119,4 +144,3 @@ x(#modbus{transport = #modbus_transport_tcp{host = Host, port = Port, timeout =
2000 2000
end, end,
gen_tcp:connect(Host, Port, [binary], Timeout). gen_tcp:connect(Host, Port, [binary], Timeout).

View File

@ -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; access_log /var/log/modbus_access.log;
} }
@ -34,7 +34,7 @@ device boiler_controller {
retry_timeout 2s; retry_timeout 2s;
# 数据点定义 # 数据点定义
variables { metrics {
# 温度读取(保持寄存器) # 温度读取(保持寄存器)
temperature { temperature {
address 40001; # Modbus地址表示法 address 40001; # Modbus地址表示法
@ -54,16 +54,16 @@ device boiler_controller {
} }
# 状态位(线圈) # 状态位(线圈)
alarm_status { #alarm_status {
address 00001; # address 00001;
type bool; # type bool;
bits { # bits {
0 "overheat"; # 0 "overheat";
1 "low_pressure"; # 1 "low_pressure";
2 "pump_failure"; # 2 "pump_failure";
} # }
poll on; # poll on;
} #}
} }
# 写入控制 # 写入控制
@ -100,13 +100,13 @@ device xyz {
retry_timeout 2s; retry_timeout 2s;
# 数据点定义 # 数据点定义
variables { metrics {
# 温度读取(保持寄存器) # 温度读取(保持寄存器)
temperature { temperature {
address 40001; # Modbus地址表示法 address 40001; # Modbus地址表示法
type int16; type int16;
scale 0.1; scale 0.1;
unit "°C"; unit "° C";
poll on; poll on;
} }
@ -120,16 +120,16 @@ device xyz {
} }
# 状态位(线圈) # 状态位(线圈)
alarm_status { #alarm_status {
address 00001; # address 00001;
type bool; # type bool;
bits { # bits {
0 "overheat"; # 0 "overheat";
1 "low_pressure"; # 1 "low_pressure";
2 "pump_failure"; # 2 "pump_failure";
} # }
poll on; # poll on;
} #}
} }
# 写入控制 # 写入控制