ast parser
This commit is contained in:
parent
6eadee5bd2
commit
27a87c29d8
@ -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, {
|
||||||
@ -30,9 +78,4 @@
|
|||||||
scale = 1.0 :: float(),
|
scale = 1.0 :: float(),
|
||||||
unit :: string() | undefined,
|
unit :: string() | undefined,
|
||||||
poll = true :: boolean()
|
poll = true :: boolean()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-type modbus_ast() :: #{
|
|
||||||
transport => #modbus_transport{},
|
|
||||||
devices => [#modbus_device{}]
|
|
||||||
}.
|
|
||||||
@ -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.
|
|
||||||
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"),
|
{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
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