From 92ea32568b7eed78e8650eda6fff983cd91a36a7 Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Fri, 20 Jun 2025 16:02:44 +0800 Subject: [PATCH] fix --- README.md | 3 + apps/modbus/include/modbus_ast.hrl | 15 +++- apps/modbus/src/modbus_device.erl | 99 ++++++++++++++++++++++ apps/modbus/src/modbus_logger.erl | 128 +++++++++++++++++++++++++++++ apps/modbus/src/modbus_parser.erl | 39 +++++++-- apps/modbus/src/modbus_service.erl | 36 ++++++-- modbus.conf | 48 +++++------ 7 files changed, 327 insertions(+), 41 deletions(-) create mode 100644 apps/modbus/src/modbus_device.erl create mode 100644 apps/modbus/src/modbus_logger.erl diff --git a/README.md b/README.md index 040939e..1594f96 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ modbus An OTP application +## 编译serial可执行文件 + gcc serial.c -o serial + Build ----- diff --git a/apps/modbus/include/modbus_ast.hrl b/apps/modbus/include/modbus_ast.hrl index 718f84f..c2925ac 100644 --- a/apps/modbus/include/modbus_ast.hrl +++ b/apps/modbus/include/modbus_ast.hrl @@ -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(), diff --git a/apps/modbus/src/modbus_device.erl b/apps/modbus/src/modbus_device.erl new file mode 100644 index 0000000..a4c09a6 --- /dev/null +++ b/apps/modbus/src/modbus_device.erl @@ -0,0 +1,99 @@ +%%%------------------------------------------------------------------- +%%% @author anlicheng +%%% @copyright (C) 2025, +%%% @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 +%%%=================================================================== diff --git a/apps/modbus/src/modbus_logger.erl b/apps/modbus/src/modbus_logger.erl new file mode 100644 index 0000000..95dffec --- /dev/null +++ b/apps/modbus/src/modbus_logger.erl @@ -0,0 +1,128 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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. \ No newline at end of file diff --git a/apps/modbus/src/modbus_parser.erl b/apps/modbus/src/modbus_parser.erl index 844e8a8..49cf9c2 100644 --- a/apps/modbus/src/modbus_parser.erl +++ b/apps/modbus/src/modbus_parser.erl @@ -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). @@ -287,4 +306,10 @@ map_of_time(Key, M, Def) -> end; false -> Def - end. \ No newline at end of file + 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, "'"). \ No newline at end of file diff --git a/apps/modbus/src/modbus_service.erl b/apps/modbus/src/modbus_service.erl index 48d0c45..4161284 100644 --- a/apps/modbus/src/modbus_service.erl +++ b/apps/modbus/src/modbus_service.erl @@ -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 = <>, + %% 建立连接 + 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). - diff --git a/modbus.conf b/modbus.conf index e9317fd..47ef56c 100644 --- a/modbus.conf +++ b/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; + #} } # 写入控制