ast parser

This commit is contained in:
anlicheng 2025-06-11 23:18:04 +08:00
parent 6eadee5bd2
commit 27a87c29d8
5 changed files with 521 additions and 109 deletions

View File

@ -8,20 +8,68 @@
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-author("anlicheng"). -author("anlicheng").
-record(modbus_transport, { -record(modbus_transport_rtu, {
type :: rtu | tcp,
port :: string(), port :: string(),
baudrate :: integer() | undefined, baudrate :: integer(),
host :: string() | undefined, parity :: any(),
timeout :: integer() | undefined 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, { -record(modbus_device, {
name :: atom(), %%
name :: string(),
%%
slave_id :: integer(), slave_id :: integer(),
model :: string() | undefined,
description :: string() | undefined,
%%
poll_interval :: integer() | undefined,
%%
retries :: integer(),
retry_timeout :: integer(),
%%
variables = #{} :: map(), 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, { -record(modbus_var, {
@ -31,8 +79,3 @@
unit :: string() | undefined, unit :: string() | undefined,
poll = true :: boolean() poll = true :: boolean()
}). }).
-type modbus_ast() :: #{
transport => #modbus_transport{},
devices => [#modbus_device{}]
}.

View File

@ -19,14 +19,15 @@
%% %%
parse(Input) when is_binary(Input) -> parse(Input) when is_binary(Input) ->
Tokens = lexer(Input), Tokens = lexer(Input),
R = parser(Tokens), {ok, AST} = parser(Tokens),
lager:debug("parse result is: ~p", [R]). lists:foreach(fun(E) ->
lager:debug("block: ~p", [E])
end, parse_ast(AST)),
%{ok, AST} = parser(Tokens), case validate(AST) of
%case validate(AST) of ok -> {ok, AST};
% ok -> {ok, AST}; {error, Reason} -> {error, Reason}
% {error, Reason} -> {error, Reason} end.
%end.
parse_file(Filename) -> parse_file(Filename) ->
case file:read_file(Filename) of case file:read_file(Filename) of
@ -87,15 +88,14 @@ is_special(_) -> false.
%% AST %% AST
parser(Tokens) -> parser(Tokens) ->
%display_tokens(Tokens), parser(Tokens, []).
{_, B} = parser_block0(Tokens), parser([], TopBlocks) ->
{ok, B}. {ok, lists:reverse(TopBlocks)};
parser(Tokens, TopBlocks) ->
display_tokens(Tokens) -> {ResetTokens, B} = parser_block0(Tokens),
lists:foreach(fun(T) -> lager:debug("token is: ~p", [T]) end, Tokens). parser(ResetTokens, [B|TopBlocks]).
%% tokens解析成block, {ResetToken, Block} %% tokens解析成block, {ResetToken, Block}
%% %%
parser_block0([{comment, _, _}|Tokens]) -> parser_block0([{comment, _, _}|Tokens]) ->
parser_block0(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([{comment, _, _}|Tokens], B) ->
parser_block0(Tokens, B). parser_block0(Tokens, B).
%% , : {ResetTokens, Props} %% ast
%% Block的关闭字符 parse_ast(Blocks) ->
parser_props([{special, _, $}}|Tokens], 0, Props) -> [parse_ast0(B) || B <- Blocks].
{Tokens, lists:reverse(Props)}; parse_ast0(#block{ident = <<"modbus">>, props = Props}) ->
parser_props([{special, _, $}}|Tokens], Level, Props) -> MapProps = parse_ast1(Props),
lager:debug("call level: ~p, me here: ~p", [Level, Props]), #modbus {
parser_props(Tokens, Level - 1, Props); 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) -> parse_ast1(Props) ->
parser_props(Tokens, Level, Props); parse_ast1(Props, []).
%% parse_ast1([], Acc) ->
parser_props([{ident, _, <<>>}|Tokens], Level, Props) -> maps:from_list(lists:reverse(Acc));
parser_props(Tokens, Level, Props); parse_ast1([Bin|T], Acc) when is_binary(Bin) ->
[Name|Vars] = binary:split(Bin, <<" ">>, [global, trim]),
%% parse_ast1(T, [{Name, Vars}|Acc]);
parser_props([{ident, _, <<"transport", Proto/binary>>}, {special, _, ${} | Tokens], Level, Props) -> parse_ast1([#block{ident = <<"actions">>, props = Props}|T], Acc) ->
{RestTokens, ChildProps} = parser_props(Tokens, 1, []), parse_ast1(T, [{<<"actions">>, Props}|Acc]);
parse_ast1([#block{ident = <<"recovery_actions">>, props = Props}|T], Acc) ->
display_tokens(Tokens), parse_ast1(T, [{<<"recovery_actions">>, Props}|Acc]);
parse_ast1([#block{ident = <<"transform">>, props = Props}|T], Acc) ->
parser_props(RestTokens, Level - 1, [{block, {transport, Proto}, ChildProps}|Props]); parse_ast1(T, [{<<"transform">>, Props}|Acc]);
parser_props([{ident, _, <<"variables">>}, {special, _, ${}|Tokens], Level, Props) -> parse_ast1([#block{ident = <<"variables">>, props = Props}|T], Acc) ->
{RestTokens, ChildProps} = parser_props(Tokens, 1, []), parse_ast1(T, [{<<"variables">>, Props}|Acc]);
parser_props(RestTokens, Level + 1, [{block, variables, ChildProps}|Props]); parse_ast1([#block{ident = <<"controls">>, props = Props}|T], Acc) ->
%% parse_ast1(T, [{<<"controls">>, Props}|Acc]);
parser_props([{ident, _, BlockName}, {special, _, ${}|Tokens], Level, Props) -> parse_ast1([#block{ident = <<"transport", Name0/binary>>, props = Props}|T], Acc) ->
{RestTokens, ChildProps} = parser_props(Tokens, 1, []), PropsMap = parse_ast1(Props),
parser_props(RestTokens, Level + 1, [{block, BlockName, ChildProps}|Props]); Transport = case string:trim(Name0) of
<<"rtu">> ->
%% : port /dev/ttyUSB0; #modbus_transport_rtu{
parser_props([{ident, _Line, Prop}, {special, _, $;}|Tokens], Level, Props) -> port = maps:get(<<"port">>, PropsMap, undefined),
parser_props(Tokens, Level, [Prop|Props]); baudrate = maps:get(<<"baudrate">>, PropsMap, undefined),
parity = maps:get(<<"parity">>, PropsMap, undefined),
%% stopbits = maps:get(<<"stopbits">>, PropsMap, undefined),
parser_props([{comment, _, _}|Tokens], Level, Props) -> timeout = maps:get(<<"timeout">>, PropsMap, undefined)
parser_props(Tokens, Level, Props). };
<<"tcp">> ->
parse_value([{ident, _, Value}|Tokens], _) -> {binary_to_atom(Value, utf8), Tokens}; #modbus_transport_tcp {
parse_value([{integer, _, Value}|Tokens], _) -> {Value, Tokens}; host = maps:get(<<"host">>, PropsMap, undefined),
parse_value([{float, _, Value}|Tokens], _) -> {Value, Tokens}; port = maps:get(<<"port">>, PropsMap, undefined),
parse_value([{string, _, Value}|Tokens], _) -> {Value, Tokens}; timeout = maps:get(<<"timeout">>, PropsMap, undefined)
parse_value([{'{', _}|Tokens], Acc) -> }
{Block, Rest} = parse_block(Tokens, []), end,
{Block, Rest}. parse_ast1(T, [{<<"transport">>, Transport}|Acc]).
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的合法性和一致性 %% AST的合法性和一致性
validate(AST) -> validate([]) ->
try ok;
validate_transport(AST), validate([#modbus{transport = #modbus_transport_rtu{port = Port, baudrate = Baudrate}}|T]) ->
validate_devices(AST), case filelib:is_file(Port) andalso Baudrate > 0 of
ok true ->
catch validate(T);
throw:Reason -> {error, Reason} false ->
end. throw({invalid_transport_rtu, {Port, Baudrate}})
validate_transport(#{transport := #modbus_transport{type = rtu, port = Port}}) ->
case filelib:is_file(Port) of
true -> ok;
false -> throw({invalid_port, Port})
end; end;
validate_transport(#{transport := #modbus_transport{type = tcp, host = Host}}) -> validate([#modbus{transport = #modbus_transport_tcp{host = Host, port = Port}}|T]) ->
case inet:parse_address(Host) of case Host /= undefined andalso Port > 0 of
{ok, _} -> ok; true ->
_ -> throw({invalid_host, Host}) validate(T);
false ->
throw({invalid_transport_tcp, {Host, Port}})
end; end;
validate_transport(_) -> throw(missing_transport_config). validate([#modbus{transport = undefined}|_]) ->
throw({empty_transport});
validate_devices(#{devices := Devices}) -> validate([#modbus_device{slave_id = Id}|_]) when Id < 1 orelse Id > 247 ->
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}); throw({invalid_slave_id, Id});
validate_device(#modbus_device{variables = Vars}) -> validate([#modbus_device{variables = _Vars}|T]) ->
maps:foreach(fun validate_variable/2, Vars). validate(T);
validate([_|T]) ->
validate_variable(_Name, #modbus_var{address = Addr}) when Addr < 0 orelse Addr > 65535 -> validate(T).
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.

View File

@ -0,0 +1,219 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @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(<<Char/utf8, Rest/binary>>, 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.

View File

@ -16,7 +16,5 @@ test() ->
{ok, Config} = file:read_file("/usr/local/code/cloudkit/modbus/modbus.conf"), {ok, Config} = file:read_file("/usr/local/code/cloudkit/modbus/modbus.conf"),
%lager:debug("config is: ~ts", [Config]), %lager:debug("config is: ~ts", [Config]),
{ok, AST} = modbus_parser:parse(Config), {ok, AST} = modbus_parser:parse(Config),
modbus_parser:validate(AST),
lager:debug("ast is: ~p", [AST]), lager:debug("ast is: ~p", [AST]),
ok. ok.

135
modbus_bak.conf Normal file
View File

@ -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";
}
}