ast parser
This commit is contained in:
parent
6eadee5bd2
commit
27a87c29d8
@ -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{}]
|
||||
}.
|
||||
}).
|
||||
@ -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.
|
||||
validate([#modbus_device{variables = _Vars}|T]) ->
|
||||
validate(T);
|
||||
validate([_|T]) ->
|
||||
validate(T).
|
||||
219
apps/modbus/src/modbus_parser_v1.erl.bak
Normal file
219
apps/modbus/src/modbus_parser_v1.erl.bak
Normal 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.
|
||||
@ -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.
|
||||
135
modbus_bak.conf
Normal file
135
modbus_bak.conf
Normal 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";
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user