一、   引子

监控画面的主要功能之一就是跟踪下位机变量变化,并将这些变化展现为动画。大部分时候,界面上一个图元组件的某个状态,与单一变量Tag绑定,比如电机的运行态,绑定一个MotorRunning信号;但有些时候不会这么简单,比如温度计在温度高于50℃显示红色;某设备报警,可能是多个条件其中之一触发的结果;变量变化触发一系列连锁反应…如此种种。考虑到工控行业大部分技术人员并非计算机专业出身,如何能够用最少的编码解决各种复杂的变量-动画绑定问题,无疑要费一番心思。

二、   方案选型

针对变量动画绑定问题,可以选择的方案包括如下几种:

  • 脚本编译器

不少大型组态软件包含强大的脚本编辑器,支持诸如VBS、Python甚至C脚本语言。脚本自带语法编辑器、调试器和编译器,调用的API包罗万象,如数据库API,通讯API,画面组态API…可以用脚本实现非常复杂的逻辑。

但基于下面几种考虑,我没有实现这类的脚本编译器:

  1. 不同于大部分组态软件包含一个独立的界面设计器,我用Visual Studio来肩挑语法编辑、调试、编译和界面设计的重任,没必要多此一举的搞一个独立的脚本编译器。
  2. C#结合Visual Studio来调用通讯、数据库链接的各类函数,C#包含强大的语法功能,配合.NET 类库几乎无所不能,同时C#也支持脚本化,没有必要在使用其他脚本语言。

对于复杂的逻辑,就让C#配合VS神器来完成吧。

  • 运算符重载。

曾经研究过一个C#写的脚本编译系统,它可以实现两个特定集合间的四则运算和逻辑运算,如List1.A+List2.A;List1.A>List2.B。看上去集合就像一个普通的数值那样参与运算和操作。

运算符重载是C#一个强大的语法功能,可以重载的操作符如下:

运算符

可重载性

+、-、!、~、++、--、true、false

可以重载这些一元运算符。
  true和false运算符必须成对重载。

+、-、*、/、%、&、|、^、<<、>>

可以重载这些二元运算符。

==、!=、<、>、<=、>=

可以重载比较运算符。必须成对重载。

&&、||

不能重载条件逻辑运算符。
  但可以使用能够重载的&和|进行计算。

[]

不能重载数组索引运算符,但可以定义索引器。

()

不能重载转换运算符,但可以定义新的转换运算符。

+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=

不能显式重载赋值运算符。
  在重写单个运算符如+、-、%时,它们会被隐式重写。

=、.、?:、->、new、is、sizeof、typeof

 

无疑运算符重载用的好可以写出语义更清晰、更简洁的代码。

比如有一种复数类型Complex,有两个坐标x和y;定义ComplexA大于ComplexB为: A的x,y中至少有一个大于B的x,y。我只需要重载>操作符(相应的最好重载>=,<,<=),以后只需要A>B就能代替重复啰嗦的A.x>B.x||A.y>B.y。更可喜的是,重载后的>,<这些运算符,在.Net表达式树(ExpressionTree)中已经替换了它原来的语义。因此运算符重载在我这个编译器也有它用武之地。

但出于下面两个原因,它只适合作为编译引擎的辅助,而不适合单独使用:

  1. 首先运算符重载只针对特定的类型;对于不熟悉C#语法特性的编程者,理解并正确的使用运算符重载不是件容易的事。
  2. 运算符重载可以减少重复的代码,让语法更简洁;但依然要写C#代码,不适合大部分工控人员。
  •  订阅事件

如果想省事,最简单的办法是直接写代码,例如:如果一台电机的运行需要A,B,C三个前提条件均满足,我就分别订阅A、B、C的变量变化事件,如果A由fasle变为true,再看看其他两个变量触发没有。也就是写这样几行代码:

            var tag1 = App.Server["A"];
var tag2 = App.Server["B"];
var tag3 = App.Server["C"];
if (tag1 != null && tag2 != null && tag3 != null
{
tag1.ValueChanged += (s, e) =>
{
if (tag1.Value.Boolean && tag2.Value.Boolean && tag3.Value.Boolean)
{
//执行
}
};
tag2.ValueChanged += (s, e) =>
{
if (tag1.Value.Boolean && tag2.Value.Boolean && tag3.Value.Boolean)
{
//执行
}
};
tag3.ValueChanged += (s, e) =>
{
if (tag1.Value.Boolean && tag2.Value.Boolean && tag3.Value.Boolean)
{
//执行
}
};
}

看上去不算复杂吧?如果界面上有50个动画,这样的代码就要写50次。不但浪费时间,改起来麻烦,查起来也麻烦。更糟糕的是,不懂编程的人还用不了。

  • 表达式编译器

对于大部分零编程基础的上位机设计人员,他们需要的是一种没有学习和理解成本的、简单直观的变量绑定方式。

比如温度计在温度高于50℃显示红色,就一句话【temperature>50】;某设备显示报警,可能是多个报警变量其中之一触发的结果,只需写【Alarm1||Alarm2||Alarm3】…借助微软强大的表达式引擎,如果能解析这类变量表达式,设计者只需要知道图元与变量的逻辑关系;而极少数表达式也难以企及的功能,略微懂一点C#就可以实现。这样就可以做到使用简单,上手容易,同时又可以满足复杂的需求。

同时还有下面几个额外的好处:

最少的编码量:在一个界面的cs文件里,几乎没有代码。绑定逻辑在XAML内用直观的方式嵌入:

  1. 可以用复制、粘贴和文本替换等功能减少重复编码;
  2. 可以充分利用WPF的设计器扩展,实现一个简单的语法编辑器,实现语法高亮、自动完成并执行语法检查;
  3. 查找变量逻辑和修改很方便。

这个编译器的主要代码在Eval类。

三、   自己实现一个编译器

  • 编译原理

大学计算机都有一门编译原理课程。当年我也捧着一本教材,被“波兰表达式”、“逆波兰表达式”绕的云里雾里,然而逆波兰表达式是实现编译器的关键。

逆波兰表达式的优势在于只用两种简单操作,入栈和出栈就可以搞定任何普通表达式的运算。其运算方式如下:

如果当前字符为变量或者为数字,则压栈,如果是运算符,则将栈顶两个元素弹出作相应运算,结果再入栈,最后当表达式扫描完后,栈里的就是结果。

如何实现自己的编译器,微软已经给大家现成的轮子了。微软的Expression类提供了一套拼接、编译Lambda表达式的完整方法,可以用它轻松定义你自己的语法。相关知识可以参考博客园 装配脑袋的自己动手开发编译器系列文章:http://www.cnblogs.com/Ninputer/archive/2011/06/18/2084383.html。下面就以这个SCADA项目为例:

  • 定义语法

在这一版,我只实现了最基本最常用的一些操作,如四则运算(+-*/)、逻辑运算(&|!)、取反取模、三目条件等运算。

GetOperatorLevel函数按照C#的运算符优先级定义运算优先级。

定义了@开头的自定义函数如@Date取当前日期、@App取当前路径等。

IsConstant方法定义系统常数,其中True/False表示逻辑常量,字符串常量用’’。

  • 编译过程

编译过程就是将一个字符串转换为一个带返回值的函数;函数的参数就是表达式相关的Tag的值。依次为:

  1. RpnExpression方法:将中缀表达式转换为逆波兰表达式。用关键字将表达式字符串分割为一个数组;按照优先级出栈入栈;返回一个逆波兰表达式顺序的字符串列表。
  2. ComplieRpnExp方法:根据逆波兰表达式顺序,依次弹出运算符转换为Expression的各子类如二元表达式BinaryExpression、条件表达式ConditionalExpression、常数表达式ConstantExpression等;参数首先判断是否常数,如果不是,则调用GetTagExpression方法,将字符串转换为方法调用MethodCallExpression,最终会将该参数编译为一个Tag。经过处理最终返回一个LambdaExpression
  3. Eval方法将LambdaExpression编译为一个委托;相关的Tag加入列表TagList。

四、   应用场景

  •   表达式与动画绑定

在每一个界面窗体都有几乎一样的几行代码:

       List<TagNodeHandle> _valueChangedList;

        private void HMI_Loaded(object sender, RoutedEventArgs e)
{
lock (this)
{
_valueChangedList = cvs1.BindingToServer(App.Server);
}
} private void HMI_Unloaded(object sender, RoutedEventArgs e)
{
lock (this)
{
App.Server.RemoveHandles(_valueChangedList);
}
}

其中, BindingToServer就是对当前界面所有图元进行地毯式扫描,搜索出各控件相关的TagReadText表达式并用Eval类编译之;编译的结果转换为带返回值的函数和一个相关Tag的列表;遍历这个Tag列表,将其值变化事件ValueChanged与这个函数链接起来。这样,在加载界面的时候已经完成了编译过程,相关变量的值一旦改变,就会根据表达式返回一个值,如果这个值是布尔量,同时与电机的运行动画绑定,就完成了从表达式到动画的触发过程。

  •   复杂报警条件

报警一般包括超限报警、变量触发报警、差值报警等。但也可能有复杂的报警条件,不能用超限、超差等简单方式表述的,就可以归结为复杂报警,其条件可以用类似动画绑定的表达式来描述,在系统初始化时刻加载、编译为报警条件。

  •  未来改进

编辑器改进:支持命令自动完成、语法高亮、更完善的语法检查。可考虑Sharpdevelop的编辑控件。

支持复杂语法:目前的语法仅仅是简单的四则运算和逻辑表达式。未来考虑支持多段表达式、函数(如正余弦)、属性引用等复杂语法。

五、   下面的计划

写一系列帖子,把架构、原理讲清楚。大致如下:

github地址:https://github.com/GavinYellow/SharpSCADA。QQ群:102486275

开源纯C#工控网关+组态软件(八)表达式编译器的更多相关文章

  1. 开源纯C#工控网关+组态软件

    一.   前言 在园子潜水也七八年了.说来惭愧,这么多年虽然一直自称.NET铁杆粉丝,然仅限于回几个不痛不痒的贴,既没有发布过代码,也没有写过文章. 看着.NET和C#在国外风生水起,国内却日趋没落, ...

  2. 开源纯C#工控网关+组态软件(二)工控网关的实现

    一.   工控网关是什么 网关是物联网和工控系统的核心组件.网关起的是承上启下的作用.上即上位机,电脑/触屏监控系统.MES这些:下即下位机,包括PLC.传感器.嵌入式芯片等. 不同厂家的下位机,往往 ...

  3. 开源纯C#工控网关+组态软件(七)数据采集与归档

    一.   引子 在当前自动化.信息化.智能化的时代背景下,数据的作用日渐凸显.而工业发展到如今,科技含量和自动化水平均显著提高,但对数据的采集.利用才开始起步. 对工业企业而言,数据采集日益受到重视, ...

  4. 开源纯C#工控网关+组态软件(九)定制Visual Studio

    一.   引子 因为最近很忙(lan),很久没发博了.不少朋友对那个右键弹出菜单和连线的功能很感兴趣,因为VS本身是不包含这种功能的.   大家想这是什么鬼,怎么我的设计器没有,其实这是一个微软黑科技 ...

  5. 开源纯C#工控网关+组态软件(十)移植到.NET Core

    一.   引子 写这个开源系列已经十来篇了.自从十年前注册博客园以来,关注了张善友.老赵.xiaotie.深蓝色右手等一众大牛,也围观了逗比的吉日嘎啦.精密顽石等形形色色的园友.然而整整十年一篇文章都 ...

  6. 开源纯C#工控网关+组态软件(五)从网关到人机界面

    一.   引子 之前都在讲网关,不少网友关注如何实现界面.想了解下位机变量变化,是怎样一步步触发人机界面动画的. 这个步步触发,实质上是变量组(Group)的批量数据变化(DataChange)事件, ...

  7. 开源纯C#工控网关+组态软件(三)加入一个新驱动:西门子S7

    一.   引子 首先感谢博客园:第一篇文章.第一个开源项目,算是旗开得胜.可以看到,项目大部分流量来自于博客园,码农乐园,名不虚传^^. 园友给了我很多支持,并提出了很好的改进意见.现加入屏幕分辨率自 ...

  8. 开源纯C#工控网关+组态软件(四)上下位机通讯原理

    一.   网关的功能:承上启下 最近有点忙,更新慢了.感谢园友们给予的支持,现在github上已经有.目标是最好的开源组态,看来又近一步^^ 之前有提到网关是物联网的关键环节,它的作用就是承上启下. ...

  9. 开源纯C#工控网关+组态软件(六)图元组件

    一.   图元概述 图元是构成人机界面的基本单元.如一个个的电机.设备.数据显示.仪表盘,都是图元.构建人机界面的过程就是铺排.挪移.定位图元的过程. 图元设计是绘图和编码的结合.因为图元不仅有显示和 ...

随机推荐

  1. Kotlin——最详细的操作符与操作符重载详解(上)

    本篇文章为大家详细的介绍Koltin特有的操作符重载.或许对于有编程经验的朋友来说,操作符这个词绝对不陌生,就算没有任何编辑基础的朋友,数学中的算数运算符也绝不陌生.例如(+.-.*./.>.& ...

  2. js 对象的值传递

    一.变量赋值的不同 1.原始值 在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的. 2.引用值: 在将一个保存着对象内存地址的变量复制给另一个变量 ...

  3. Python正则表达式返回首次匹配到的字符及查询的健壮性

    re.findall(pattern,string)会搜索所有匹配的字符,返回的是一个列表,获取首个匹配需要re.findall(pattern,string)[0]访问, 但是如果findall没匹 ...

  4. flush table with read lock的轻量级解决方案[原创]

    为什么要使用FTWRL   MySQL dba在日常工作中,数据备份绝对是工作频度最高的工作内容之一.当你使用逻辑方式进行备份(mydumper,mysqldump)或物理方式进行备份(percona ...

  5. Linux磁盘分区/格式化/挂载(树莓派3挂载硬盘)

    [前言] 本文将要讲解的功能为Linux环境下对磁盘进行操作.包括分区.格式化.挂载外接移动存储设备等. 该文的写作背景为本人的树莓派需要外接一个固态硬盘作为存储设备,因此,便开始了一系列的折腾. [ ...

  6. 扒一扒offsetleft,srollleft,pagex,clientx,postion().left等精确位置的获取与理解

    先上个pc端和手机端的图:   说明:上面的属性,都是in这个div的属性值.我是点击的in这个div的左上角,所以pageX.pageY是40. HTML: <div class=" ...

  7. 关于mybatis-generator的问题

    1.运行完mybatis-generator没有出现问题,但是代码看不到,出现这种东西: 你需要使用相对路径,如项目名+/src/main/java,就可以解决了 2.附录我的代码以供参考: < ...

  8. 变量、交互&注释、数字&字符串&布尔、格式化输出

    变量 变量定义规范: 声明变量: name = "Neo Zheng"    # name为变量名(标识符),"Neo Zheng"是变量值. 变量定义规则: ...

  9. Elastic FileBeat 快速入门

    背景 用过ELK(Elasticsearch, Logstash, Kibana)的人应该都面临过同样的问题,Logstash虽然功能强大:支持许多的input/output plugin.强大的fi ...

  10. docker创建ceph集群

    背景 Ceph官方现在提供两类镜像来创建集群,一种是常规的,每一种Ceph组件是单独的一个镜像,如ceph/daemon.ceph/radosgw.ceph/mon.ceph/osd等:另外一种是最新 ...