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