diff --git a/apps/modbus/include/modbus_ast.hrl b/apps/modbus/include/modbus_ast.hrl index 10a9fb9..c4f92c3 100644 --- a/apps/modbus/include/modbus_ast.hrl +++ b/apps/modbus/include/modbus_ast.hrl @@ -8,20 +8,68 @@ %%%------------------------------------------------------------------- -author("anlicheng"). --record(modbus_transport, { - type :: rtu | tcp, +-record(modbus_transport_rtu, { port :: string(), - baudrate :: integer() | undefined, - host :: string() | undefined, - timeout :: integer() | undefined + baudrate :: integer(), + parity :: any(), + stopbits :: integer(), + timeout = 0 :: integer() +}). + +-record(modbus_transport_tcp, { + host :: string(), + port :: string(), + timeout = 0 :: integer() +}). + +-record(modbus, { + transport :: #modbus_transport_rtu{} | #modbus_transport_tcp{}, + error_log = "" :: string(), + access_log = "" :: string() }). -record(modbus_device, { - name :: atom(), + %% 设备名称 + name :: string(), + %% 设备标识 slave_id :: integer(), + + model :: string() | undefined, + description :: string() | undefined, + + %% 轮询间隔 + poll_interval :: integer() | undefined, + + %% 重试策略 + retries :: integer(), + retry_timeout :: integer(), + + %% 数据定义 variables = #{} :: map(), - controls = #{} :: map(), - poll_interval :: integer() | undefined + + controls = #{} :: map() +}). + +-record(modbus_processor, { + name :: string(), + input :: string(), + transform :: any(), + + output :: [] +}). + +-record(modbus_alarm, { + name :: string(), + condition :: string(), + + %% 持续判定 + hold_time :: string(), + + %% 动作 + actions :: [], + + %% 恢复动作 + recovery_actions = [] }). -record(modbus_var, { @@ -30,9 +78,4 @@ scale = 1.0 :: float(), unit :: string() | undefined, poll = true :: boolean() -}). - --type modbus_ast() :: #{ - transport => #modbus_transport{}, - devices => [#modbus_device{}] -}. \ No newline at end of file +}). \ No newline at end of file diff --git a/apps/modbus/src/modbus_parser.erl b/apps/modbus/src/modbus_parser.erl index bd6c7f5..351227f 100644 --- a/apps/modbus/src/modbus_parser.erl +++ b/apps/modbus/src/modbus_parser.erl @@ -19,14 +19,15 @@ %% 主解析函数 parse(Input) when is_binary(Input) -> Tokens = lexer(Input), - R = parser(Tokens), - lager:debug("parse result is: ~p", [R]). + {ok, AST} = parser(Tokens), + lists:foreach(fun(E) -> + lager:debug("block: ~p", [E]) + end, parse_ast(AST)), - %{ok, AST} = parser(Tokens), - %case validate(AST) of - % ok -> {ok, AST}; - % {error, Reason} -> {error, Reason} - %end. + case validate(AST) of + ok -> {ok, AST}; + {error, Reason} -> {error, Reason} + end. parse_file(Filename) -> case file:read_file(Filename) of @@ -87,15 +88,14 @@ is_special(_) -> false. %% 语法分析:将标记序列转换为AST parser(Tokens) -> - %display_tokens(Tokens), - {_, B} = parser_block0(Tokens), - {ok, B}. - -display_tokens(Tokens) -> - lists:foreach(fun(T) -> lager:debug("token is: ~p", [T]) end, Tokens). + parser(Tokens, []). +parser([], TopBlocks) -> + {ok, lists:reverse(TopBlocks)}; +parser(Tokens, TopBlocks) -> + {ResetTokens, B} = parser_block0(Tokens), + parser(ResetTokens, [B|TopBlocks]). %% 将tokens解析成block, 返回值格式为:{ResetToken, Block} - %% 忽略掉注释信息 parser_block0([{comment, _, _}|Tokens]) -> parser_block0(Tokens); @@ -120,89 +120,106 @@ parser_block0([{ident, _Line, Prop}, {special, _, $;}|Tokens], B = #block{props parser_block0([{comment, _, _}|Tokens], B) -> parser_block0(Tokens, B). -%% 解析属性, 返回值: {ResetTokens, Props} -%% 这里很重要,要到Block的关闭字符 -parser_props([{special, _, $}}|Tokens], 0, Props) -> - {Tokens, lists:reverse(Props)}; -parser_props([{special, _, $}}|Tokens], Level, Props) -> - lager:debug("call level: ~p, me here: ~p", [Level, Props]), - parser_props(Tokens, Level - 1, Props); +%% 转换成ast +parse_ast(Blocks) -> + [parse_ast0(B) || B <- Blocks]. +parse_ast0(#block{ident = <<"modbus">>, props = Props}) -> + MapProps = parse_ast1(Props), + #modbus { + transport = maps:get(<<"transport">>, MapProps, undefined), + access_log = maps:get(<<"access_log">>, MapProps, undefined), + error_log = maps:get(<<"error_log">>, MapProps, undefined) + }; +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), + 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), + retry_timeout = maps:get(<<"retry_timeout">>, MapProps, undefined), + variables = maps:get(<<"variables">>, MapProps, undefined), + controls = maps:get(<<"controls">>, MapProps, undefined) + }; +parse_ast0(#block{ident = <<"processor", Name0/binary>>, props = Props}) -> + MapProps = parse_ast1(Props), + #modbus_processor { + name = string:trim(Name0), + input = maps:get(<<"input">>, MapProps, undefined), + transform = maps:get(<<"transform">>, MapProps, undefined) + }; +parse_ast0(#block{ident = <<"alarm", Name0/binary>>, props = Props}) -> + MapProps = parse_ast1(Props, []), + #modbus_alarm { + name = string:trim(Name0), + condition = maps:get(<<"condition">>, MapProps, undefined), + hold_time = maps:get(<<"hold_time">>, MapProps, undefined), + actions = maps:get(<<"actions">>, MapProps, undefined), + recovery_actions = maps:get(<<"recovery_actions">>, MapProps, undefined) + }. -parser_props([{special, _, $;}|Tokens], Level, Props) -> - parser_props(Tokens, Level, Props); -%% 处理空的定义 -parser_props([{ident, _, <<>>}|Tokens], Level, Props) -> - parser_props(Tokens, Level, Props); - -%% 允许被嵌套的定义 -parser_props([{ident, _, <<"transport", Proto/binary>>}, {special, _, ${} | Tokens], Level, Props) -> - {RestTokens, ChildProps} = parser_props(Tokens, 1, []), - - display_tokens(Tokens), - - parser_props(RestTokens, Level - 1, [{block, {transport, Proto}, ChildProps}|Props]); -parser_props([{ident, _, <<"variables">>}, {special, _, ${}|Tokens], Level, Props) -> - {RestTokens, ChildProps} = parser_props(Tokens, 1, []), - parser_props(RestTokens, Level + 1, [{block, variables, ChildProps}|Props]); -%% 非标准定义 -parser_props([{ident, _, BlockName}, {special, _, ${}|Tokens], Level, Props) -> - {RestTokens, ChildProps} = parser_props(Tokens, 1, []), - parser_props(RestTokens, Level + 1, [{block, BlockName, ChildProps}|Props]); - -%% 其他定义,是基于: port /dev/ttyUSB0; 这样的格式的 -parser_props([{ident, _Line, Prop}, {special, _, $;}|Tokens], Level, Props) -> - parser_props(Tokens, Level, [Prop|Props]); - -%% 忽略掉注释信息 -parser_props([{comment, _, _}|Tokens], Level, Props) -> - parser_props(Tokens, Level, Props). - -parse_value([{ident, _, Value}|Tokens], _) -> {binary_to_atom(Value, utf8), Tokens}; -parse_value([{integer, _, Value}|Tokens], _) -> {Value, Tokens}; -parse_value([{float, _, Value}|Tokens], _) -> {Value, Tokens}; -parse_value([{string, _, Value}|Tokens], _) -> {Value, Tokens}; -parse_value([{'{', _}|Tokens], Acc) -> - {Block, Rest} = parse_block(Tokens, []), - {Block, Rest}. - -parse_block([{'}', _}|Tokens], Props) -> {lists:reverse(Props), Tokens}; -parse_block([{ident, Line, Name}|Tokens], Props) -> - {Value, Rest} = parse_value(Tokens, []), - parse_block(Rest, Props#{binary_to_atom(Name, utf8) => Value}). +parse_ast1(Props) -> + parse_ast1(Props, []). +parse_ast1([], Acc) -> + maps:from_list(lists:reverse(Acc)); +parse_ast1([Bin|T], Acc) when is_binary(Bin) -> + [Name|Vars] = binary:split(Bin, <<" ">>, [global, trim]), + parse_ast1(T, [{Name, Vars}|Acc]); +parse_ast1([#block{ident = <<"actions">>, props = Props}|T], Acc) -> + parse_ast1(T, [{<<"actions">>, Props}|Acc]); +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 = <<"controls">>, props = Props}|T], Acc) -> + parse_ast1(T, [{<<"controls">>, Props}|Acc]); +parse_ast1([#block{ident = <<"transport", Name0/binary>>, props = Props}|T], Acc) -> + PropsMap = parse_ast1(Props), + Transport = case string:trim(Name0) of + <<"rtu">> -> + #modbus_transport_rtu{ + port = maps:get(<<"port">>, PropsMap, undefined), + baudrate = maps:get(<<"baudrate">>, PropsMap, undefined), + parity = maps:get(<<"parity">>, PropsMap, undefined), + stopbits = maps:get(<<"stopbits">>, PropsMap, undefined), + timeout = maps:get(<<"timeout">>, PropsMap, undefined) + }; + <<"tcp">> -> + #modbus_transport_tcp { + host = maps:get(<<"host">>, PropsMap, undefined), + port = maps:get(<<"port">>, PropsMap, undefined), + timeout = maps:get(<<"timeout">>, PropsMap, undefined) + } + end, + parse_ast1(T, [{<<"transport">>, Transport}|Acc]). %% 语义验证:检查AST的合法性和一致性 -validate(AST) -> - try - validate_transport(AST), - validate_devices(AST), - ok - catch - throw:Reason -> {error, Reason} - end. - -validate_transport(#{transport := #modbus_transport{type = rtu, port = Port}}) -> - case filelib:is_file(Port) of - true -> ok; - false -> throw({invalid_port, Port}) +validate([]) -> + ok; +validate([#modbus{transport = #modbus_transport_rtu{port = Port, baudrate = Baudrate}}|T]) -> + case filelib:is_file(Port) andalso Baudrate > 0 of + true -> + validate(T); + false -> + throw({invalid_transport_rtu, {Port, Baudrate}}) end; -validate_transport(#{transport := #modbus_transport{type = tcp, host = Host}}) -> - case inet:parse_address(Host) of - {ok, _} -> ok; - _ -> throw({invalid_host, Host}) +validate([#modbus{transport = #modbus_transport_tcp{host = Host, port = Port}}|T]) -> + case Host /= undefined andalso Port > 0 of + true -> + validate(T); + false -> + throw({invalid_transport_tcp, {Host, Port}}) end; -validate_transport(_) -> throw(missing_transport_config). +validate([#modbus{transport = undefined}|_]) -> + throw({empty_transport}); -validate_devices(#{devices := Devices}) -> - lists:foreach(fun validate_device/1, Devices). - -validate_device(#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}); -validate_device(#modbus_device{variables = Vars}) -> - maps:foreach(fun validate_variable/2, Vars). - -validate_variable(_Name, #modbus_var{address = Addr}) when Addr < 0 orelse Addr > 65535 -> - throw({invalid_register_address, Addr}); -validate_variable(_Name, #modbus_var{type = Type}) - when Type /= int16 andalso Type /= uint16 andalso Type /= float32 -> - throw({invalid_data_type, Type}); -validate_variable(_, _) -> ok. \ No newline at end of file +validate([#modbus_device{variables = _Vars}|T]) -> + validate(T); +validate([_|T]) -> + validate(T). \ No newline at end of file diff --git a/apps/modbus/src/modbus_parser_v1.erl.bak b/apps/modbus/src/modbus_parser_v1.erl.bak new file mode 100644 index 0000000..39e7d15 --- /dev/null +++ b/apps/modbus/src/modbus_parser_v1.erl.bak @@ -0,0 +1,219 @@ +%%%------------------------------------------------------------------- +%%% @author anlicheng +%%% @copyright (C) 2025, +%%% @doc +%%% +%%% @end +%%% Created : 10. 6月 2025 22:08 +%%%------------------------------------------------------------------- +-module('modbus_parser_v1.erl'). + +-export([parse/1, parse_file/1]). +-include("modbus_ast.hrl"). + +%% 主解析函数 +parse(Input) when is_binary(Input) -> + Tokens = lexer(Input), + {ok, AST} = parser(Tokens), + case validate(AST) of + ok -> {ok, AST}; + {error, Reason} -> {error, Reason} + end. + +parse_file(Filename) -> + case file:read_file(Filename) of + {ok, Content} -> parse(Content); + Error -> Error + end. + +%% 词法分析:将输入文本转换为标记序列 +lexer(Input) -> + lexer(Input, 1, [], []). + +lexer(<<>>, _Line, _Current, Acc) -> + lists:reverse(Acc); +lexer(<<$\s, Rest/binary>>, Line, Current, Acc) -> + lexer(Rest, Line, [" "|Current], Acc); +lexer(<<$\n, Rest/binary>>, Line, Current, Acc) -> + lexer(Rest, Line + 1, Current, Acc); +lexer(<<$\t, Rest/binary>>, Line, Current, Acc) -> + lexer(Rest, Line, [" "|Current], Acc); +lexer(<<$#, Rest/binary>>, Line, _Current, Acc) -> + {Comment, NewRest} = read_until(Rest, <<$\n>>), + lexer(NewRest, Line + 1, [], [{comment, Line, Comment}|Acc]); +%% 处理特殊字符开头的行, ‘$=’ 不会是一行的开头 +lexer(<<${, Rest/binary>>, Line, [], Acc) -> + lexer(Rest, Line, [], [{special, Line, ${}|Acc]); +lexer(<<$}, Rest/binary>>, Line, [], Acc) -> + lexer(Rest, Line, [], [{special, Line, $}}|Acc]); +lexer(<<$;, Rest/binary>>, Line, [], Acc) -> + lexer(Rest, Line, [], [{special, Line, $;}|Acc]); +%% 在行中遇到特殊字符的处理逻辑 +lexer(<>, Line, Current, Acc) -> + case is_special(Char) of + true -> + case Current of + [] -> + lexer(Rest, Line, [], [{special, Line, Char}|Acc]); + _ -> + Ident = list_to_binary(string:trim(lists:reverse(Current))), + lexer(Rest, Line, [], [{special, Line, Char}, {ident, Line, Ident}|Acc]) + end; + false -> + lexer(Rest, Line, [Char|Current], Acc) + end. + +read_until(Bin, Delim) when is_binary(Bin), is_binary(Delim) -> + case binary:match(Bin, Delim) of + {Pos, _} -> + {binary:part(Bin, 0, Pos), binary:part(Bin, Pos + 1, byte_size(Bin) - Pos - 1)}; + nomatch -> + {Bin, <<>>} + end. + +is_special(${) -> true; +is_special($}) -> true; +is_special($;) -> true; +is_special($=) -> true; +is_special(_) -> false. + +%% 语法分析:将标记序列转换为AST +parser(Tokens) -> + %display_tokens(Tokens), + parser_block(Tokens, 0, []). + +display_tokens(Tokens) -> + lists:foreach(fun(T) -> lager:debug("token is: ~p", [T]) end, Tokens). + +%% 将tokens解析成block, 返回值格式为:{ResetToken, Block} +parser_block([], 0, Blocks0) -> + Blocks = lists:reverse(Blocks0), + lager:debug("parse result: ~p", [Blocks]), + {ok, Blocks}; + +%% 这里很重要,要到Block的关闭字符 +parser_block([{special, _, $}}|Tokens], 1, Blocks) -> + parser_block(Tokens, 0, Blocks); +parser_block([{special, _, $}}|Tokens], Level, Blocks) -> + parser_block(Tokens, Level - 1, Blocks); + +parser_block([{special, _, $;}|Tokens], Level, Blocks) -> + parser_block(Tokens, Level, Blocks); +%% 处理空的定义 +parser_block([{ident, _, <<>>}|Tokens], Level, Blocks) -> + parser_block(Tokens, Level, Blocks); + +%% 预定义支持的标签, 顶层定义, 遇到是,目前的level值必须等于:0 +parser_block([{ident, _, <<"modbus">>}, {special, _, ${} |Tokens], 0, Blocks) -> + {RestTokens, Props} = parser_props(Tokens, 1, []), + lager:debug("modbus: ~p", [Props]), + parser_block(RestTokens, 0, [{block, modbus, Props}|Blocks]); +parser_block([{ident, _, <<"device", Name/binary>>}, {special, _, ${}|Tokens], 0, Blocks) -> + {RestTokens, Props} = parser_props(Tokens, 1, []), + parser_block(RestTokens, 0, [{block, {device, string:trim(Name)}, Props}|Blocks]); +parser_block([{ident, _, <<"processor", Name/binary>>}, {special, _, ${}|Tokens], 0, Blocks) -> + {RestTokens, Props} = parser_props(Tokens, 1, []), + parser_block(RestTokens, 0, [{block, {processor, string:trim(Name)}, Props}|Blocks]); +parser_block([{ident, _, <<"alarm", Name/binary>>}, {special, _, ${}|Tokens], 0, Blocks) -> + {RestTokens, Props} = parser_props(Tokens, 1, []), + parser_block(RestTokens, 0, [{block, {alarm, string:trim(Name)}, Props}|Blocks]); + +%% 其他定义,是基于: port /dev/ttyUSB0; 这样的格式的 +parser_block([{ident, _Line, Prop}, {special, _, $;}|Tokens], Level, [{block, Block, Props}|Blocks]) -> + parser_block(Tokens, Level, [{block, Block, [Prop|Props]}|Blocks]); + +%% todo +%parser([{ident, _Line, Name}, {special, _, $=}|Tokens], [{block, _, Props}|Stack]) -> +% {Value, Rest} = parse_value(Tokens, []), +% parser(Rest, [{block, Props#{binary_to_atom(Name, utf8) => Value}}|Stack]); + +%% 忽略掉注释信息 +parser_block([{comment, _, _}|Tokens], Level, Stack) -> + parser_block(Tokens, Level, Stack). + +%% 解析属性, 返回值: {ResetTokens, Props} +%% 这里很重要,要到Block的关闭字符 +parser_props([{special, _, $}}|Tokens], 0, Props) -> + {Tokens, lists:reverse(Props)}; +parser_props([{special, _, $}}|Tokens], Level, Props) -> + lager:debug("call level: ~p, me here: ~p", [Level, Props]), + parser_props(Tokens, Level - 1, Props); + +parser_props([{special, _, $;}|Tokens], Level, Props) -> + parser_props(Tokens, Level, Props); +%% 处理空的定义 +parser_props([{ident, _, <<>>}|Tokens], Level, Props) -> + parser_props(Tokens, Level, Props); + +%% 允许被嵌套的定义 +parser_props([{ident, _, <<"transport", Proto/binary>>}, {special, _, ${} | Tokens], Level, Props) -> + {RestTokens, ChildProps} = parser_props(Tokens, 1, []), + + display_tokens(Tokens), + + parser_props(RestTokens, Level - 1, [{block, {transport, Proto}, ChildProps}|Props]); +parser_props([{ident, _, <<"variables">>}, {special, _, ${}|Tokens], Level, Props) -> + {RestTokens, ChildProps} = parser_props(Tokens, 1, []), + parser_props(RestTokens, Level + 1, [{block, variables, ChildProps}|Props]); +%% 非标准定义 +parser_props([{ident, _, BlockName}, {special, _, ${}|Tokens], Level, Props) -> + {RestTokens, ChildProps} = parser_props(Tokens, 1, []), + parser_props(RestTokens, Level + 1, [{block, BlockName, ChildProps}|Props]); + +%% 其他定义,是基于: port /dev/ttyUSB0; 这样的格式的 +parser_props([{ident, _Line, Prop}, {special, _, $;}|Tokens], Level, Props) -> + parser_props(Tokens, Level, [Prop|Props]); + +%% 忽略掉注释信息 +parser_props([{comment, _, _}|Tokens], Level, Props) -> + parser_props(Tokens, Level, Props). + +parse_value([{ident, _, Value}|Tokens], _) -> {binary_to_atom(Value, utf8), Tokens}; +parse_value([{integer, _, Value}|Tokens], _) -> {Value, Tokens}; +parse_value([{float, _, Value}|Tokens], _) -> {Value, Tokens}; +parse_value([{string, _, Value}|Tokens], _) -> {Value, Tokens}; +parse_value([{'{', _}|Tokens], Acc) -> + {Block, Rest} = parse_block(Tokens, []), + {Block, Rest}. + +parse_block([{'}', _}|Tokens], Props) -> {lists:reverse(Props), Tokens}; +parse_block([{ident, Line, Name}|Tokens], Props) -> + {Value, Rest} = parse_value(Tokens, []), + parse_block(Rest, Props#{binary_to_atom(Name, utf8) => Value}). + +%% 语义验证:检查AST的合法性和一致性 +validate(AST) -> + try + validate_transport(AST), + validate_devices(AST), + ok + catch + throw:Reason -> {error, Reason} + end. + +validate_transport(#{transport := #modbus_transport{type = rtu, port = Port}}) -> + case filelib:is_file(Port) of + true -> ok; + false -> throw({invalid_port, Port}) + end; +validate_transport(#{transport := #modbus_transport{type = tcp, host = Host}}) -> + case inet:parse_address(Host) of + {ok, _} -> ok; + _ -> throw({invalid_host, Host}) + end; +validate_transport(_) -> throw(missing_transport_config). + +validate_devices(#{devices := Devices}) -> + lists:foreach(fun validate_device/1, Devices). + +validate_device(#modbus_device{slave_id = Id}) when Id < 1 orelse Id > 247 -> + throw({invalid_slave_id, Id}); +validate_device(#modbus_device{variables = Vars}) -> + maps:foreach(fun validate_variable/2, Vars). + +validate_variable(_Name, #modbus_var{address = Addr}) when Addr < 0 orelse Addr > 65535 -> + throw({invalid_register_address, Addr}); +validate_variable(_Name, #modbus_var{type = Type}) + when Type /= int16 andalso Type /= uint16 andalso Type /= float32 -> + throw({invalid_data_type, Type}); +validate_variable(_, _) -> ok. \ No newline at end of file diff --git a/apps/modbus/src/test.erl b/apps/modbus/src/test.erl index 36dc462..b0c237f 100644 --- a/apps/modbus/src/test.erl +++ b/apps/modbus/src/test.erl @@ -16,7 +16,5 @@ test() -> {ok, Config} = file:read_file("/usr/local/code/cloudkit/modbus/modbus.conf"), %lager:debug("config is: ~ts", [Config]), {ok, AST} = modbus_parser:parse(Config), - - modbus_parser:validate(AST), lager:debug("ast is: ~p", [AST]), ok. \ No newline at end of file diff --git a/modbus_bak.conf b/modbus_bak.conf new file mode 100644 index 0000000..1e4c78c --- /dev/null +++ b/modbus_bak.conf @@ -0,0 +1,135 @@ +modbus { + # 通信参数 + transport rtu { + port /dev/ttyUSB0; + baudrate 9600; + parity none; + stopbits 1; + timeout 1s; + } + + # 或TCP配置 + transport tcp { + host 192.168.1.100; + port 502; + timeout 500ms; + } + + # 日志设置 + error_log /var/log/modbus_error.log warn; + access_log /var/log/modbus_access.log; +} + +device boiler_controller { + # 设备标识 + 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; + + # 数据转换 + transform { + # 线性转换: raw * 0.1 + 2.5 + linear 0.1 2.5; + + # 滤波 + moving_average 5; + + # 范围检查 + validate { + min -10.0; + max 150.0; + action log; # or 'discard', 'replace_with: value' + } + } + + # 输出目标 + #output { + # mqtt "sensors/boiler/temp"; + # influxdb "plant_metrics" measurement="temperature"; + #} +} + +alarm high_temperature { + # 触发条件 + condition $boiler_controller.temperature > 90.0; + + # 持续判定 + hold_time 30s; + + # 动作 + actions { + log "CRITICAL: Boiler temperature too high!"; + mqtt "alarms/boiler"; + email "ops@example.com"; + exec "/usr/local/bin/shutdown_boiler.sh"; + } + + # 恢复动作 + recovery_actions { + log "Boiler temperature back to normal"; + } +} \ No newline at end of file