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


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服務器引擎。

聯系作者及加群方式(激活碼在群里發放):http://www.hslcommunication.cn/Cooperation

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;

接下就可以愉快碼代碼了。

 

 

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的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕
                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的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕
                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的讀取,讀取到數據后需要自己做一些轉換,根據類型來自己轉,參照上面類型未知的節點操作代碼。

            try
            {
                // 添加所有的讀取的節點,此處的示例是類型不一致的情況
                List<NodeId> nodeIds = new List<NodeId>( );
                nodeIds.Add( new NodeId( "ns=2;s=Devices/分廠一/車間二/ModbusTcp客戶端/溫度" ) );
                nodeIds.Add( new NodeId( "ns=2;s=Devices/分廠一/車間二/ModbusTcp客戶端/風俗" ) );
                nodeIds.Add( new NodeId( "ns=2;s=Devices/分廠一/車間二/ModbusTcp客戶端/轉速" ) );
                nodeIds.Add( new NodeId( "ns=2;s=Devices/分廠一/車間二/ModbusTcp客戶端/機器人關節" ) );
                nodeIds.Add( new NodeId( "ns=2;s=Devices/分廠一/車間二/ModbusTcp客戶端/cvsdf" ) );
                nodeIds.Add( new NodeId( "ns=2;s=Devices/分廠一/車間二/ModbusTcp客戶端/條碼" ) );
                nodeIds.Add( new NodeId( "ns=2;s=Devices/分廠一/車間二/ModbusTcp客戶端/開關量" ) );

                // dataValues按順序定義的值,每個值里面需要重新判斷類型
                List<DataValue> dataValues = opcUaClient.ReadNodes( nodeIds.ToArray() );
                // 然后遍歷你的數據信息
                foreach (var dataValue in dataValues)
                {
                    // 獲取你的實際的數據
                    object value = dataValue.WrappedValue.Value;
                }




                // 如果你批量讀取的值的類型都是一樣的,比如float,那么有簡便的方式
                List<string> tags = new List<string>( );
                tags.Add( "ns=2;s=Devices/分廠一/車間二/ModbusTcp客戶端/風俗" );
                tags.Add( "ns=2;s=Devices/分廠一/車間二/ModbusTcp客戶端/轉速" );

                // 按照順序定義的值
                List<float> values = opcUaClient.ReadNodes<float>( tags.ToArray() );

            }
            catch (Exception ex)
            {
                ClientUtils.HandleException( this.Text, ex );
            }

  

 

 

節點寫入操作


 

節點的寫入操作和讀取類似,我們還是必須要先知道節點的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的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕
                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的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕
                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的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕
                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的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕
                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的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕
                ClientUtils.HandleException(Text, ex);
            }
        }
        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的錯誤處理機制來處理錯誤,網絡不通或是讀取拒絕
                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; }
        }

 

 

特別說明


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

 


免責聲明!

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



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