C# 讀寫opc ua服務器,瀏覽所有節點,讀寫節點,讀歷史數據,調用方法,訂閱,批量訂閱操作


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; }
}
復制代碼

特別說明


雖然提供了刪除一個節點和新增一個節點的方法,但是在客戶端是不允許操作的,調用無效。

 

原文轉自:https://www.cnblogs.com/dathlin/p/7724834.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM