diff --git a/apps/modbus/include/modbus_ast.hrl b/apps/modbus/include/modbus_ast.hrl index c4f92c3..718f84f 100644 --- a/apps/modbus/include/modbus_ast.hrl +++ b/apps/modbus/include/modbus_ast.hrl @@ -78,4 +78,11 @@ scale = 1.0 :: float(), unit :: string() | undefined, poll = true :: boolean() +}). + +-record(ast, { + modbus, + devices = [], + processors = [], + alarms = [] }). \ No newline at end of file diff --git a/apps/modbus/src/modbus_parser.erl b/apps/modbus/src/modbus_parser.erl index 4e92d78..841e076 100644 --- a/apps/modbus/src/modbus_parser.erl +++ b/apps/modbus/src/modbus_parser.erl @@ -19,9 +19,42 @@ %% 主解析函数 parse(Input) when is_binary(Input) -> Tokens = lexer(Input), - {ok, AST} = parser(Tokens), - try validate(AST) of + {ok, Blocks} = parser(Tokens), + Trees = parse_ast(Blocks), + try validate(Trees) of ok -> + %% 分组 + Modbus = lists:filter(fun(E) -> + case E of + #modbus{} -> true; + _ -> false + end + end, Trees), + Devices = lists:filter(fun(E) -> + case E of + #modbus_device{} -> + true; + _ -> false + end + end, Trees), + Processors = lists:filter(fun(E) -> + case E of + #modbus_processor{} -> + true; + _ -> + false + end + end, Trees), + Alarms = lists:filter(fun(E) -> + case E of + #modbus_alarm{} -> + true; + _ -> + false + end + end, Trees), + + AST = #ast{modbus = Modbus, devices = Devices, processors = Processors, alarms = Alarms }, {ok, AST} catch throw:Reason -> {error, Reason} @@ -124,11 +157,11 @@ parse_ast0(#block{ident = <<"device", Name0/binary>>, props = Props}) -> MapProps = parse_ast1(Props), #modbus_device { name = string:trim(Name0), - slave_id = maps:get(<<"slave_id">>, MapProps, undefined), + slave_id = map_of_integer(<<"slave_id">>, MapProps, 0), model = maps:get(<<"model">>, MapProps, undefined), description = maps:get(<<"description">>, MapProps, undefined), poll_interval = maps:get(<<"poll_interval">>, MapProps, undefined), - retries = maps:get(<<"retries">>, MapProps, undefined), + retries = map_of_integer(<<"retries">>, MapProps, 0), retry_timeout = maps:get(<<"retry_timeout">>, MapProps, undefined), variables = maps:get(<<"variables">>, MapProps, undefined), controls = maps:get(<<"controls">>, MapProps, undefined) @@ -212,4 +245,18 @@ validate([#modbus_device{slave_id = Id}|_]) when Id < 1 orelse Id > 247 -> validate([#modbus_device{variables = _Vars}|T]) -> validate(T); validate([_|T]) -> - validate(T). \ No newline at end of file + validate(T). + +-spec starts_with(Bin :: binary(), Prefix :: binary()) -> boolean(). +starts_with(Bin, Prefix) when is_binary(Bin), is_binary(Prefix) -> + binary:part(Bin, 0, byte_size(Prefix)) == Prefix. + +map_of_integer(Key, M, Def) -> + case maps:is_key(Key, M) of + true -> + [Val0] = maps:get(Key, M), + binary_to_integer(Val0); + false -> + Def + end. + diff --git a/apps/modbus/src/modbus_service.erl b/apps/modbus/src/modbus_service.erl new file mode 100644 index 0000000..8d26537 --- /dev/null +++ b/apps/modbus/src/modbus_service.erl @@ -0,0 +1,100 @@ +%%%------------------------------------------------------------------- +%%% @author anlicheng +%%% @copyright (C) 2025, +%%% @doc +%%% +%%% @end +%%% Created : 13. 6月 2025 09:46 +%%%------------------------------------------------------------------- +-module(modbus_service). +-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]). + +-define(SERVER, ?MODULE). + +-record(state, { + +}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +%% @doc Spawns the server and registers the local name (unique) +-spec(start_link(AST :: #ast{}) -> + {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). +start_link(AST) -> + gen_server:start_link({local, ?SERVER}, ?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([]) -> + {ok, #state{}}. + +%% @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/modbus.conf b/modbus.conf index 1e4c78c..e9317fd 100644 --- a/modbus.conf +++ b/modbus.conf @@ -86,6 +86,72 @@ device boiler_controller { } } +device xyz { + # 设备标识 + slave_id 1; + model "Siemens S7-1200"; + description "Main boiler controller"; + + # 轮询间隔 + poll_interval 5s; + + # 重试策略 + retries 3; + retry_timeout 2s; + + # 数据点定义 + variables { + # 温度读取(保持寄存器) + temperature { + address 40001; # Modbus地址表示法 + type int16; + scale 0.1; + unit "°C"; + poll on; + } + + # 压力传感器(输入寄存器) + pressure { + address 30001; + type uint16; + scale 0.01; + unit "kPa"; + poll on; + } + + # 状态位(线圈) + alarm_status { + address 00001; + type bool; + bits { + 0 "overheat"; + 1 "low_pressure"; + 2 "pump_failure"; + } + poll on; + } + } + + # 写入控制 + controls { + # 启停控制 + power_switch { + address 00010; + type bool; + safe_value off; + } + + # PID设定值 + pid_setpoint { + address 40010; + type float32; + min 0.0; + max 100.0; + precision 0.1; + } + } +} + processor temperature_processor { # 输入源 input $boiler_controller.temperature;