1、 引子html
监控画面的主要功能之一就是跟踪下位机变量变化,并将这些变化展示为动画。大部分时候,界面上一个图元组件的某个状态,与单一变量Tag绑定,好比电机的运行态,绑定一个MotorRunning信号;但有些时候不会这么简单,好比温度计在温度高于50℃显示红色;某设备报警,多是多个条件其中之一触发的结果;变量变化触发一系列连锁反应…如此种种。考虑到工控行业大部分技术人员并不是计算机专业出身,如何可以用最少的编码解决各类复杂的变量-动画绑定问题,无疑要费一番心思。git
2、 方案选型github
针对变量动画绑定问题,能够选择的方案包括以下几种:数据库
很多大型组态软件包含强大的脚本编辑器,支持诸如VBS、Python甚至C脚本语言。脚本自带语法编辑器、调试器和编译器,调用的API一应俱全,如数据库API,通信API,画面组态API…能够用脚本实现很是复杂的逻辑。express
但基于下面几种考虑,我没有实现这类的脚本编译器:编程
对于复杂的逻辑,就让C#配合VS神器来完成吧。数组
曾经研究过一个C#写的脚本编译系统,它能够实现两个特定集合间的四则运算和逻辑运算,如List1.A+List2.A;List1.A>List2.B。看上去集合就像一个普通的数值那样参与运算和操做。架构
运算符重载是C#一个强大的语法功能,能够重载的操做符以下:编辑器
运算符函数 |
可重载性 |
+、-、!、~、++、--、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)中已经替换了它原来的语义。所以运算符重载在我这个编译器也有它用武之地。
但出于下面两个缘由,它只适合做为编译引擎的辅助,而不适合单独使用:
若是想省事,最简单的办法是直接写代码,例如:若是一台电机的运行须要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内用直观的方式嵌入:
这个编译器的主要代码在Eval类。
3、 本身实现一个编译器
大学计算机都有一门编译原理课程。当年我也捧着一本教材,被“波兰表达式”、“逆波兰表达式”绕的云里雾里,然而逆波兰表达式是实现编译器的关键。
逆波兰表达式的优点在于只用两种简单操做,入栈和出栈就能够搞定任何普通表达式的运算。其运算方式以下:
若是当前字符为变量或者为数字,则压栈,若是是运算符,则将栈顶两个元素弹出做相应运算,结果再入栈,最后当表达式扫描完后,栈里的就是结果。
如何实现本身的编译器,微软已经给你们现成的轮子了。微软的Expression类提供了一套拼接、编译Lambda表达式的完整方法,能够用它轻松定义你本身的语法。相关知识能够参考博客园 装配脑壳的本身动手开发编译器系列文章:http://www.cnblogs.com/Ninputer/archive/2011/06/18/2084383.html。下面就以这个SCADA项目为例:
在这一版,我只实现了最基本最经常使用的一些操做,如四则运算(+-*/)、逻辑运算(&|!)、取反取模、三目条件等运算。
GetOperatorLevel函数按照C#的运算符优先级定义运算优先级。
定义了@开头的自定义函数如@Date取当前日期、@App取当前路径等。
IsConstant方法定义系统常数,其中True/False表示逻辑常量,字符串常量用’’。
编译过程就是将一个字符串转换为一个带返回值的函数;函数的参数就是表达式相关的Tag的值。依次为:
4、 应用场景
在每个界面窗体都有几乎同样的几行代码:
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的编辑控件。
支持复杂语法:目前的语法仅仅是简单的四则运算和逻辑表达式。将来考虑支持多段表达式、函数(如正余弦)、属性引用等复杂语法。
5、 下面的计划
写一系列帖子,把架构、原理讲清楚。大体以下:
github地址:https://github.com/GavinYellow/SharpSCADA。QQ群:102486275