OPC是應用於工業通信的,在windows環境的下一種通訊技術,原有的通信技術難以滿足日益復雜的環境,在可擴展性,安全性,跨平台性方面的不足日益明顯,所以OPC基金會在幾年前提出了面向未來的架構設計的OPC 統一架構,簡稱OPC UA,截止目前為止,越來越多公司將OPC UA作為開放的數據標准,在未來工業4.0行業上也將大放異彩。
在OPC UA的服務器端。會公開一些數據節點,或是方法等信息,允許第三方使用標准的OPC協議來進行訪問,在傳輸層已經安全的處理所有的消息,對於客戶端的訪問來說,應該是非常清楚簡單的。
本篇文章是講述如何開發C#的OPC UA客戶端的方式,關於如何開發OPC UA可配置的服務器,請參照另一篇博客:http://www.cnblogs.com/dathlin/p/8976955.html 這篇博客講述了如何創建基於三菱,西門子,歐姆龍,ModbusTcp客戶端,異形ModbusTcp客戶端的OPC UA服務器引擎。
2.0版本說明
2018年8月18日 20:09:24 基於OPC UA的最新官方庫,重新調整了訂閱的代碼實現,開源地址:https://github.com/dathlin/OpcUaHelper 除了組件的源代碼之外,還包含了一個服務器的示例,就是下面的的示例操作。
更加詳細的代碼說明可以參照GitHub上的readme文件
前期准備
准備好開發的IDE,首選Visual Studio2017版本,新建項目,或是在你原有的項目上進行擴展。注意:項目的.NET Framework版本最低為4.6
打開NuGet管理器,輸入指令(如果不明白,參考http://www.cnblogs.com/dathlin/p/7705014.html):
Install-Package OpcUaHelper
或者:
然后在窗體的界面新增引用:
using OpcUaHelper;
接下就可以愉快碼代碼了。
技術支持QQ群:592132877 (組件的版本更新細節也將第一時間在群里發布)
OPC UA服務器准備
此處有一個供網友測試的服務器:opc.tcp://118.24.36.220:62547/DataAccessServer
當然,一般的網友都會使用Kepware軟件,在此處介紹一個我自己開發的OPC UA網關服務器,支持三菱,西門子,歐姆龍,modbustcp客戶端轉化成OPC UA服務器,支持創建modbus服務器,異形服務器,地址是
https://github.com/dathlin/SharpNodeSettings
節點瀏覽器
我們在得到一個OPC UA的服務器之后,第一件事就是使用節點瀏覽器對所有的節點進行訪問,不然你根本就不知道服務器公開了什么東西,此處我使用了一個測試服務器,該地址為雲端地址,不保證以后會不會繼續支持訪問,目前來說還是可以訪問的。
比如這個地址:opc.tcp://118.24.36.220:62547/DataAccessServer
OK,然后我們可以使用代碼來顯示這個服務器到底有什么數據了!在窗體上新增一個按鈕,雙擊它進入點擊事件,寫上
private void button1_Click(object sender, EventArgs e) { using (FormBrowseServer form = new FormBrowseServer()) { form.ShowDialog(); } }
然后就會顯示如下的界面:在地址欄輸入上述地址,點擊連接(此處能連接上的條件是服務器配置為允許匿名登錄):
左邊區域可以隨便點擊看看,可以看到所有公開的數據,比如點擊一個數據節點,下面圖片中的Name節點,右邊編輯框會顯示該節點的ID標識,這個標識很重要,關系到等會的讀寫操作。
客戶端實例化
private OpcUaClient opcUaClient = new OpcUaClient(); private async void Form1_Load(object sender, EventArgs e) { await opcUaClient.ConnectServer("opc.tcp://118.24.36.220:62547/DataAccessServer"); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { opcUaClient.Disconnect(); }
如上所示,在窗體載入的時候實例化,在窗體關閉的時候斷開連接。下面的節點操作和其他操作使用的實例都是這個opcUaClient,如果你連接的服務器是需要用戶名和密碼的,那么修改Load中的代碼如下:
private async void Form1_Load(object sender, EventArgs e) { opcUaClient.UserIdentity = new Opc.Ua.UserIdentity("admin", "123456"); await opcUaClient.ConnectServer("opc.tcp://118.24.36.220:62547/DataAccessServer"); }
節點讀取操作
我們要讀取一個節點數據,有兩個信息是必須知道的
- 節點的ID標識,就是在上述節點瀏覽器中的編輯框的信息("ns=2;s=Machines/Machine A/Name")
- 節點的數據類型,這個是必須知道的,不然也不好讀取數據。(“string”)
上面的兩個信息都可以通過節點瀏覽器來獲取到信息,現在,我們已經獲取到了這兩個信息,就上面的括號里的數據,然后我們在新增一個按鈕,來讀取數據:
private void button2_Click(object sender, EventArgs e) { try { string value = opcUaClient.ReadNode<string>("ns=2;s=Machines/Machine A/Name"); MessageBox.Show(value); // 顯示測試數據 } catch(Exception ex) { // 使用了opc ua的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕 Opc.Ua.Client.Controls.ClientUtils.HandleException(Text, ex); } }
可以看到,真正的讀取數據的操作只有一行代碼,但是此處展示了一個良好的編程習慣,使用try..catch..,關於錯誤捕獲的使用以后會專門開篇文章講解。在展示一個讀取float數據類型的示例
private void button2_Click(object sender, EventArgs e) { try { float value = opcUaClient.ReadNode<float>("ns=2;s=Machines/Machine B/TestValueFloat"); MessageBox.Show(value.ToString()); // 顯示100.5 } catch(Exception ex) { // 使用了opc ua的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕 Opc.Ua.Client.Controls.ClientUtils.HandleException(Text, ex); } }
其他的類型參照這種寫法就行,哪怕是數組類型也是沒有關系的。
類型未知節點讀取操作
我們要讀取一個節點數據,假設我們只知道一個節點的ID,或者說這個節點的類型是可能變化的,那么我們需要讀取到值的同時讀取到這個數據的類型,那么代碼參照下面
- 節點的ID標識,就是在上述節點瀏覽器中的編輯
節點的數據類型最終由 value.WrappedValue.TypeInfo 來決定,有兩個屬性,是否是數組和基礎類型,下面的代碼只有int類型進行了嚴格的數組判斷,其他類型參照即可。
private void button3_Click(object sender, EventArgs e) { Opc.Ua.DataValue value = opcUaClient.ReadNode("ns=2;s=Robots/RobotA/RobotMode"); // 一個數據的類型是不是數組由 value.WrappedValue.TypeInfo.ValueRank 來決定的 // -1 說明是一個數值 // 1 說明是一維數組,如果類型BuiltInType是Int32,那么實際是int[] // 2 說明是二維數組,如果類型BuiltInType是Int32,那么實際是int[,] if (value.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.Int32) { if (value.WrappedValue.TypeInfo.ValueRank == -1) { int temp = (int)value.WrappedValue.Value; // 最終值 } else if (value.WrappedValue.TypeInfo.ValueRank == 1) { int[] temp = (int[])value.WrappedValue.Value; // 最終值 } else if (value.WrappedValue.TypeInfo.ValueRank == 2) { int[,] temp = (int[,])value.WrappedValue.Value; // 最終值 } } else if(value.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.UInt32) { uint temp = (uint)value.WrappedValue.Value; // 數組的情況參照上面的例子 } else if (value.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.Float) { float temp = (float)value.WrappedValue.Value; // 數組的情況參照上面的例子 } else if (value.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.String) { string temp = (string)value.WrappedValue.Value; // 數組的情況參照上面的例子 } else if (value.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.DateTime) { DateTime temp = (DateTime)value.WrappedValue.Value; // 數組的情況參照上面的例子 } } }
批量節點讀取操作
批量讀取節點時,有個麻煩之處在於類型不一定都是一致的,所以為了支持更加廣泛的讀取操作,只提供Opc.Ua.DataValue的讀取,讀取到數據后需要自己做一些轉換,根據類型來自己轉,參照上面類型未知的節點操作代碼。
private void button4_Click(object sender, EventArgs e) { string[] nodes = new string[] { "ns=2;s=Robots/RobotA/RobotMode", "ns=2;s=Robots/RobotA/UserFloat" }; // 因為不能保證讀取的節點類型一致,所以只提供統一的DataValue讀取,每個節點需要單獨解析 foreach(Opc.Ua.DataValue value in opcUaClient.ReadNodes(nodes)) { // 獲取到了值,具體的每個變量的解析參照上面類型不確定的解析 object data = value.WrappedValue.Value; // 下面寫你自己的操作 } }
節點寫入操作
節點的寫入操作和讀取類似,我們還是必須要先知道節點的ID和數據類型,和讀取最大的區別是,寫入的操作很有可能會失敗,因為服務器對於數據的輸入都是很敏感的,這部分權限肯定會控制的,也就是很有可能會發生寫入拒絕,此處的測試服務器允許寫入,下面舉例在Name節點寫入“abcd測試寫入啊”信息:
private void button3_Click(object sender, EventArgs e) { try { bool IsSuccess = opcUaClient.WriteNode("ns=2;s=Machines/Machine B/Name","abcd測試寫入啊"); MessageBox.Show(IsSuccess.ToString()); // 顯示True,如果成功的話 } catch(Exception ex) { // 使用了opc ua的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕 Opc.Ua.Client.Controls.ClientUtils.HandleException(Text, ex); } }
再寫個例子,寫入Float數據
private void button3_Click(object sender, EventArgs e) { try { bool IsSuccess = opcUaClient.WriteNode("ns=2;s=Machines/Machine B/TestValueFloat",123.456f); MessageBox.Show(IsSuccess.ToString()); // 顯示True,如果成功的話 } catch(Exception ex) { // 使用了opc ua的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕 Opc.Ua.Client.Controls.ClientUtils.HandleException(Text, ex); } }
要想查看是否真的寫入,可以使用節點數據瀏覽器來查看是否真的寫入。
批量節點寫入操作
寫入節點操作時,類型並不一定是統一的,所以此處提供統一的object數組寫入,需要注意,對應的節點名稱和值的類型必須一致!
private void button5_Click(object sender, EventArgs e) { // 批量寫入的代碼 string[] nodes = new string[] { "ns=2;s=Robots/RobotA/RobotMode", "ns=2;s=Robots/RobotA/UserFloat" }; object[] data = new object[] { 4, new float[]{5,3,1,5,7,8} }; // 都成功返回True,否則返回False bool result = opcUaClient.WriteNodes(nodes, data); }
數據訂閱
下面舉例說明訂閱ns=2;s=Machines/Machine B/TestValueFloat的數據,我們假設這個在服務器上是不斷變化的,按照如下的方式進行數據訂閱:
private void button2_Click( object sender, EventArgs e ) { // sub OpcUaClient.AddSubscription( "A", "ns=2;s=Machines/Machine B/TestValueFloat", SubCallback ); } private void SubCallback(string key, MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs args ) { if (InvokeRequired) { Invoke( new Action<string, MonitoredItem, MonitoredItemNotificationEventArgs>( SubCallback ), key, monitoredItem, args ); return; } if (key == "A") { // 如果有多個的訂閱值都關聯了當前的方法,可以通過key和monitoredItem來區分 MonitoredItemNotification notification = args.NotificationValue as MonitoredItemNotification; if (notification != null) { textBox3.Text = notification.Value.WrappedValue.Value.ToString( ); } } }
移除訂閱
OpcUaClient.RemoveSubscription( "A" );
批量訂閱的方式,參照源代碼或是 github的說明文件。
方法調用
有些OPC 服務器會提供方法調用,測試服務器提供了一個方法,它支持兩個int參數輸入,string參數輸出,方法節點為:ns=2;s=Machines/Machine B/Calculate
我們接下來看看調用服務器的方法到底返回了什么?
private void button6_Click(object sender, EventArgs e) { try { string value = opcUaClient.CallMethodByNodeId("ns=2;s=Machines/Machine B", "ns=2;s=Machines/Machine B/Calculate", 123, 456)[0].ToString(); MessageBox.Show(value);// 顯示:我也不知道剛剛發生了什么,調用設備為:Machine B } catch(Exception ex) { // 使用了opc ua的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕 Opc.Ua.Client.Controls.ClientUtils.HandleException(Text, ex); } }
我們在調用方法的時候需要傳入方法的父節點 ID,以及方法的ID,必須先清楚方法的傳入參數和傳出參數才能對應的代碼。
日志輸出
OPC UA客戶端在運行時會輸出一大堆的日志,容量會增加的比較快,是否需要配置,請謹慎處理,如果真的有需要,按照下面的配置方式來完成
private void button5_Click(object sender, EventArgs e) { // False 代表每次啟動清空日志,True代碼不清空,注意,該日志大小增加非常快 opcUaClient.SetLogPathName(Application.StartupPath + "\\Logs\\opc.ua.client.txt", false); }
上述的都是一些最常用的方法了,已經可以應付大多數的需求,該客戶端類還提供了一些連接啟動事件,斷開事件等等,可以滿足額外的需求。
引用讀取
這種情況比較少,比如服務器端有個MachineB節點,下面放了一些數據,如果客戶端把讀取的節點寫死一般問題也不大,應該服務器很少會改變,但是服務器真的改變了呢。。。。比如在MachineB下追加了一個數據,這種情況確實很少,但是對於我們寫成相對動態的情況來說,就很有必要,但是中間問題很多,因為新增的節點類型你是不知道的,ID也是不知道的,所以還先要讀取引用,然后在讀取數據,然后在判斷類型,進行相應的轉化。
private void button6_Click(object sender, EventArgs e) { try { Opc.Ua.ReferenceDescription[] reference = opcUaClient.BrowseNodeReference("ns=2;s=Machines/Machine B"); foreach (var refer in reference) { // 如果不是值節點,就不要了,否則下面讀取了也是沒有意義的 if (refer.NodeClass != NodeClass.Variable) { continue; } // 分別讀取數據 Opc.Ua.DataValue dataValue = opcUaClient.ReadNode((Opc.Ua.NodeId)refer.NodeId); if (dataValue.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.Boolean) { // 讀取到的是bool數據,在這里做處理 bool value = (bool)dataValue.WrappedValue.Value; } else if (dataValue.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.String) { // 讀取到的是string字符串,在這里做處理 string value = dataValue.WrappedValue.Value.ToString(); } } } catch (Exception ex) { // 使用了opc ua的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕 Opc.Ua.Client.Controls.ClientUtils.HandleException(Text, ex); } }
異步操作
在讀取寫入單個節點的功能中,提供了一個異步版本,用來方便的進行異步操作
private async void button2_Click(object sender, EventArgs e) { try { float value = await opcUaClient.ReadNodeAsync<float>("ns=2;s=Machines/Machine B/TestValueFloat"); MessageBox.Show(value.ToString()); // 顯示100.5 } catch(Exception ex) { // 使用了opc ua的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕 Opc.Ua.Client.Controls.ClientUtils.HandleException(Text, ex); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
|
private
async
void
button3_Click(
object
sender, EventArgs e)
{
try
{
bool
IsSuccess = await opcUaClient.WriteNodeAsync(
"ns=2;s=Machines/Machine B/TestValueFloat"
,123.456f);
MessageBox.Show(IsSuccess.ToString());
// 顯示True,如果成功的話
}
catch
(Exception ex)
{
// 使用了opc ua的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕
Opc.Ua.Client.Controls.ClientUtils.HandleException(Text, ex);
}
}
|
查看本地以注冊的服務器
利用官方的控件庫來實現的一個操作,允許查看本地的已經注冊的服務器。
private void button6_Click( object sender, EventArgs e ) { // 獲取本機已經注冊的服務器地址 string endpointUrl = new Opc.Ua.Client.Controls.DiscoverServerDlg( ).ShowDialog( opcUaClient.AppConfig, null ); // 獲取其他服務器注冊的地址,注意,需要該IP的安全策略配置正確 // string endpointUrl = new Opc.Ua.Client.Controls.DiscoverServerDlg( ).ShowDialog( opcUaClient.AppConfig, "192.168.0.100" ); if (!string.IsNullOrEmpty( endpointUrl )) { // 獲取到的需要操作的服務器地址 } }
觸發事件
本opc ua客戶端類,包含了幾個常用的事件,現在進行說明:
- ConnectComplete 事件:在第一次連接到服務器完成的時候觸發
- ReconnectStarting 事件:開始重新連接到服務器的時候觸發
- ReconnectComplete 事件:重新連接到服務器的時候觸發
- KeepAliveComplete 事件:因為opc ua客戶端每隔5秒會與服務器進行通訊驗證,每次驗證都會觸發該方法
- OpcStatusChange 事件:本OPC UA客戶端的終極事件,當客戶端的狀態變更都會觸發,包括了連接,重連,斷開,狀態激活,opc ua的狀態等等
事件類的完整代碼如下:
/// <summary> /// 狀態通知的消息類 /// </summary> public class OpcUaStatusEventArgs : EventArgs { /// <summary> /// 是否異常 /// </summary> public bool Error { get; set; } /// <summary> /// 時間 /// </summary> public DateTime Time { get; set; } /// <summary> /// 文本 /// </summary> public string Text { get; set; } /// <summary> /// 轉化為字符串 /// </summary> /// <returns></returns> public override string ToString() { return Error ? "[異常]" : "[正常]" + Time.ToString(" yyyy-MM-dd HH:mm:ss ") + Text; } }
獲取客戶端網絡是否正常有個屬性
/// <summary> /// Indicate the connect status /// </summary> public bool Connected { get { return m_IsConnected; } }
特別說明
雖然提供了刪除一個節點和新增一個節點的方法,但是在客戶端是不允許操作的,調用無效。