本文将会从以下几个方面介绍
1.OPCUA是什么
2.OPCUA常用的工具有那些
3.OPCUA的官网
4.使用opcua常用的方法和功能介绍
5.根据官网自己封装了一个opcuaclient类,并说明每个方法的用处
6.根据4中的opcuaclient类自己写了demo
本文所有用到的资料在此下载包括UaExpert,kepserver,demo等
需要资料的你也可以直接加我VX:z20240710. 加的时候备注CSDN。有偿
1.OPCUA是什么
OPC UA(开放式产品通信统一架构,Open Platform Communications Unified Architecture)是一种通信协议和通信架构,用于实现工业自动化系统中设备之间的数据交换和通信。它提供了一种标准化的、安全的、可扩展的机制,使得不同设备和系统能够无缝地进行数据交换和通信。广泛应用于工业自动化、制造业、物联网等其他领域,用于实现设备之间的实时数据传输、监控和控制。
2.OPCUA常用的工具有那些
客户端我一般使用UaExpert
使用方法网上可以随便搜到。比如:使用UaExpert - 水滴大海 - 博客园 (cnblogs.com)
服务端我一般使用kepserver
使用方法比如:KepServer的下载安装与使用说明_牛奶咖啡13的博客-CSDN博客
3.OPCUA的官网
OPCUA github地址是:GitHub - OPCFoundation/UA-.NETStandard: OPC Unified Architecture .NET Standard
官网截图
下载后启动UA Reference.sln即可查看官方提供的一些例子
打开后截图如下,这里就是opcua官网提供的例子,大家可以自己研究一下。
4.使用opcua常用的方法和功能介绍
①连接和断开连接,当然还有断开重连
②认证,包括没有限制、需要用户名和密码、数字证书
③数据读取:先把数据读上来,然后根据数据类型转换即可。特别注意结构体读取需要特殊处理一下
④数据写入:OPC UA在数据写入时,对数据类型有严格要求,数据类型不会自动转换,会写入失败。所以写入数据时需要指定数据类型。特别注意结构体写入需要特殊处理一下
⑤订阅:数据订阅后服务器数据变化后会自动通知到客户端。相当于把压力放在了服务器。注意:服务器对与订阅资源是有限的,有的服务器必须显式的释放订阅资源,不然会造成订阅资源耗尽而订阅失败,必须重启服务端软件才能解决。
⑥结构体数据读取
⑦结构体数据写入
5.根据官网自己封装了一个opcuaclient类
①连接和断开连接,当然还有断开重连
/// <summary>
/// 连接opcua
/// </summary>
/// <param name="serverUrl">连接地址</param>
/// <param name="useSecurity">是否启用安全连接</param>
/// <param name="sessionTimeout"></param>
/// <returns></returns>
public async Task<Session> ConnectServer(
string serverUrl,
bool useSecurity,
uint sessionTimeout = 0)
{
try
{
// disconnect from existing session.
InternalDisconnect();
// select the best endpoint.
Console.WriteLine("获取远程节点:" + serverUrl);
serverUrl = "opc.tcp://127.0.0.1:49320";
var endpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, useSecurity, 600000);
var endpointConfiguration = EndpointConfiguration.Create(m_configuration);
var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
// Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(endpoint));
Console.WriteLine("开始连接:" + serverUrl);
m_session = await Session.Create(
m_configuration,
endpoint,
false,
DisableDomainCheck,
(String.IsNullOrEmpty(SessionName)) ? m_configuration.ApplicationName : SessionName,
sessionTimeout == 0 ? DefaultSessionTimeout : sessionTimeout,
UserIdentity,
PreferredLocales);
// set up keep alive callback. 监听连接状态,但连接失败时自动重连
m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive);
Console.WriteLine("OPC连接成功:" + serverUrl + "m_session.Connected的连接状态是:" + m_session.Connected);
}
catch (Exception ex)
{
Console.WriteLine("OPC连接异常:" + serverUrl + ex.ToString());
if (m_session != null)
{
m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive);
}
else
{
//isCconnect = false;
}
}
try
{
UpdateStatus(false, DateTime.Now, "Connected, loading complex type system.");
}
catch (Exception e)
{
UpdateStatus(true, DateTime.Now, "Connected, failed to load complex type system.");
Utils.Trace(e, "Failed to load complex type system.");
}
// return the new session.
return m_session;
}
/// <summary>
/// 断开连接
/// Disconnects from the server.
/// </summary>
private void InternalDisconnect()
{
// stop any reconnect operation.
if (m_reconnectHandler != null)
{
m_reconnectHandler.Dispose();
m_reconnectHandler = null;
}
// disconnect any existing session.
if (m_session != null)
{
m_session.KeepAlive -= Session_KeepAlive;
m_session.Close(10000);
m_session = null;
}
}
/// <summary>
/// Handles a keep alive event from a session.
/// 监听连接状态,当意外连接失败时自动重连
/// </summary>
private void Session_KeepAlive(Session session, KeepAliveEventArgs e)
{
try
{
// check for events from discarded sessions.
if (!Object.ReferenceEquals(session, m_session))
{
return;
}
// start reconnect sequence on communication error.
if (Opc.Ua.ServiceResult.IsBad(e.Status))
{
if (ReconnectPeriod <= 0)
{
UpdateStatus(true, e.CurrentTime, "Communication Error ({0})", e.Status);
return;
}
UpdateStatus(true, e.CurrentTime, "Reconnecting in {0}s", ReconnectPeriod);
if (m_reconnectHandler == null)
{
if (m_ReconnectStarting != null)
{
m_ReconnectStarting(this, e);
}
m_reconnectHandler = new SessionReconnectHandler();
//重连
m_reconnectHandler.BeginReconnect(m_session, ReconnectPeriod, Server_ReconnectComplete);
}
return;
}
// update status.
UpdateStatus(false, e.CurrentTime, "Connected [{0}]", session.Endpoint.EndpointUrl);
// raise any additional notifications.
if (m_KeepAliveComplete != null)
{
m_KeepAliveComplete(this, e);
}
}
catch (Exception exception)
{
throw new Exception("Session_KeepAlive", exception);
}
}
/// <summary>
/// Handles a reconnect event complete from the reconnect handler.
/// 连接断开重连成功后执行该方法
/// </summary>
private void Server_ReconnectComplete(object sender, EventArgs e)
{
try
{
// ignore callbacks from discarded objects.
if (!Object.ReferenceEquals(sender, m_reconnectHandler))
{
return;
}
m_session = m_reconnectHandler.Session;
m_reconnectHandler.Dispose();
m_reconnectHandler = null;
// raise any additional notifications.
if (m_ReconnectComplete != null)
{
m_ReconnectComplete(this, e);
}
//isCconnect = true;
//if (!isSub)
//{
// ReadItems();
//}
}
catch (Exception exception)
{
throw new Exception("Server_ReconnectComplete", exception);
}
}
②认证,包括没有限制、需要用户名和密码、数字证书
1.先加载客户端配置(一般在初始化时候加载)
/// <summary>
///
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="m_IP"></param>
/// <param name="m_Port"></param>
public opcuahelper(string username,string password)
{
m_endpoints = new Dictionary<Uri, EndpointDescription>();
if (username != "" && password != "")
{
UserIdentity = new UserIdentity(username, password);
}
Console.WriteLine("加载客户端配置");
#region 加载客户端配置
m_application.ApplicationType = ApplicationType.Client;
m_application.ConfigSectionName = "Treasure.OPCUAClient";
//1.加载配置,如果证书不存在则生成证书
//2.把生成的证书导入到服务端,让服务器信任证书
//3.然后再次连接即可成功
// load the application configuration. 读取XML给加载configuration
m_application.LoadApplicationConfiguration("OPCUAClient.Config.xml", silent: false);
// check the application certificate. 检查证书
m_application.CheckApplicationInstanceCertificate(silent: false, minimumKeySize: 0);
m_configuration = m_application.ApplicationConfiguration;
//当证书验证失败时回调
m_configuration.CertificateValidator.CertificateValidation += CertificateValidation;
#endregion
}
/// <summary>
/// Handles the certificate validation event.当证书验证失败时回调事件
/// This event is triggered every time an untrusted certificate is received from the server.
/// </summary>
private void CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e)
{
bool certificateAccepted = true;
// ****
// Implement a custom logic to decide if the certificate should be
// accepted or not and set certificateAccepted flag accordingly.
// The certificate can be retrieved from the e.Certificate field
// ***
ServiceResult error = e.Error;
while (error != null)
{
Console.WriteLine(error);
error = error.InnerResult;
}
if (certificateAccepted)
{
Console.WriteLine("Untrusted Certificate accepted. SubjectName = {0}", e.Certificate.SubjectName);
}
e.AcceptAll = certificateAccepted;
}
2.如果opcua服务器没有认证限制或者有用户名和密码。这样配置你直接使用连接方法就可以连上。但是如果是有数字证书那么你需要到服务端把客户端发来的证书添加信任,再次连接即可连上。 如下面视频我kepserver是只启动数字认证,那么客户端的连接我需要手动信任才可。 具体看如下视频
数字认证
③数据读取:先把数据读上来,然后根据数据类型转换即可。我是都转成string赋值。特别注意结构体读取需要特殊处理一下
/// <summary>
/// OPCUA批量Tag读取
/// 把读取的数据值都转成string
/// </summary>
/// <param name="itemBases">需要读取的节点</param>
/// <returns></returns>
public void ReadNodes(List<itemModel> itemBases)
{
try
{
if (m_session.Connected)
{
//Console.WriteLine("数据节点转换");
//节点格式转换
ReadValueIdCollection nodesToRead = new ReadValueIdCollection();
for (int i = 0; i < itemBases.Count; i++)
{
nodesToRead.Add(new ReadValueId()
{
NodeId = new NodeId(itemBases[i].ItemAdress),
AttributeId = Attributes.Value
});
}
NodeId ss = itemBases[0].ItemAdress;
//EncodeableFactory.GlobalFactory.AddEncodeableType(typeof(QTI_Data));
// Read the node attributes
DataValueCollection resultsValues = null;
DiagnosticInfoCollection diagnosticInfos = null;
//Console.WriteLine("数据读取开始");
// Call Read Service
m_session.Read(
null,
0,
TimestampsToReturn.Both,
nodesToRead,
out resultsValues,
out diagnosticInfos);
//验证结果
ClientBase.ValidateResponse(resultsValues, nodesToRead);
for (int i = 0; i < resultsValues.Count; i++)
{
//这里数组还有其他的标准数据类型都当转string赋值,当然你也可以根据resultsValues[i].WrappedValue.TypeInfo一个个处理
if (resultsValues[i].Value == null)
{
itemBases[i].ItemValue = "";
}
else
{
//若是字符串数据的话,则进行直接赋值
if (resultsValues[i].WrappedValue.TypeInfo.BuiltInType == BuiltInType.String)
{
itemBases[i].ItemValue = resultsValues[i].Value.ToString();
}
else
{
itemBases[i].ItemValue = Newtonsoft.Json.JsonConvert.SerializeObject(resultsValues[i].Value);
}
}
//在这里进行结构体类型数据处理
if (itemBases[i].ItemType == DataType.Struct)
{
// itemBases[i].ItemValue = ReadStruct(itemBases[i].ItemAdress, resultsValues[i]);
}
}
}
}
catch (Exception ex)
{
throw new Exception("OPUAReadNodes:" + ex.Message + ex.StackTrace);
}
}
④数据写入:OPC UA在数据写入时,对数据类型有严格要求,数据类型不会自动转换,会写入失败。所以写入数据时需要指定数据类型。特别注意结构体写入需要特殊处理一下
/// <summary>
/// 单个写入节点,注意T的数据类型要与OPCUA服务器的数据类型相对应
/// </summary>
/// <typeparam name="T">写入的数据类型</typeparam>
/// <param name="t">写入的值</param>
/// <param name="nodeAddress">写入值的地址</param>
public void WriteNodes<T>(T t, string nodeAddress)
{
try
{
if (m_session.Connected)
{
//ApplicationLog.operateLog("数据节点转换");
//节点格式转换
WriteValueCollection nodesToWrite = new WriteValueCollection();
//if (itemBases.ItemType == DataType.Struct)//类型是结构体时
//{
// List<WriteVar> WriteVarList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<WriteVar>>(itemBases.ItemValue);
// ExtensionObject[] ExtensionObjectArr = new ExtensionObject[100];
// for (int j = 0; j < ExtensionObjectArr.Length; j++)
// {
// ExtensionObjectArr[j] = new ExtensionObject { Body = getNewByte() };
// }
// foreach (var item in WriteVarList)
// {
// var result1 = GetValueByteFromObj(item);
// ExtensionObjectArr[Convert.ToInt32(item.index)] = result1;
// }
// nodesToWrite.Add(new WriteValue()
// {
// NodeId = new NodeId(itemBases.ItemAdress),
// AttributeId = Attributes.Value,
// Value = new DataValue()
// {
// Value = ExtensionObjectArr
// }
// });
//}
nodesToWrite.Add(new WriteValue()
{
NodeId = new NodeId(nodeAddress),
AttributeId = Attributes.Value,
Value = new DataValue()
{
Value = t
}
});
//NodeId ss = itemBases.ItemAdress;
//EncodeableFactory.GlobalFactory.AddEncodeableType(typeof(QTI_Data));
// Read the node attributes
StatusCodeCollection resultsValues = null;
DiagnosticInfoCollection diagnosticInfos = null;
//ApplicationLog.operateLog("数据读取开始");
// Call Read Service
m_session.Write(
null,
nodesToWrite,
out resultsValues,
out diagnosticInfos);
//验证结果
ClientBase.ValidateResponse(resultsValues, nodesToWrite);
}
else
{
throw new Exception("OPCUA连接断开");
}
}
catch (Exception ex)
{
Console.WriteLine("WriteNodes 写入异常" + ex.ToString());
throw;
}
}
⑤订阅:数据订阅后服务器数据变化后会自动通知到客户端。相当于把压力放在了服务器。注意:服务器对与订阅资源是有限的,有的服务器必须显式的释放订阅资源,不然会造成订阅资源耗尽而订阅失败,必须重启服务端软件才能解决。 而且除非监听时去除的Subscription 对象,而非单个节点。
List<itemModel> Items = new List<itemModel>();
Subscription subscription;
/// <summary>
///Create Subscription and MonitoredItems for DataChanges
/// 添加订阅
/// </summary>
/// <param name="interval">采样间隔 单位毫秒</param>
/// <param name="itemBases"></param>
public void Subscribe(int interval, List<itemModel> itemBases)
{
try
{
// Create a subscription for receiving data change notifications
// Define Subscription parameters
subscription = new Subscription(m_session.DefaultSubscription);
subscription.DisplayName = "OPCUA Subscribe";
subscription.PublishingEnabled = true;
subscription.PublishingInterval = 200;
for (int i = 0; i < itemBases.Count; i++)
{
// Create MonitoredItems for data changes
MonitoredItem MonitoredItem = new MonitoredItem(subscription.DefaultItem);
// Int32 Node - Objects\CTT\Scalar\Simulation\Int32
MonitoredItem.StartNodeId = new NodeId(itemBases[i].ItemAdress);
MonitoredItem.AttributeId = Attributes.Value;
MonitoredItem.DisplayName = itemBases[i].ItemID;
MonitoredItem.SamplingInterval = interval;
MonitoredItem.Notification += OnMonitoredItemNotification;
subscription.AddItem(MonitoredItem);
Items.Add(itemBases[i]);
}
m_session.AddSubscription(subscription);
// Create the subscription on Server side
subscription.Create();
// Create the monitored items on Server side
//subscription.ApplyChanges();
}
catch (Exception ex)
{
Console.WriteLine("OPCUA订阅" + ex.ToString());
}
}
/// <summary>
/// Create Subscription and MonitoredItems for DataChanges
/// 移除订阅
/// </summary>
public void RemoveSubscribe()
{
try
{
m_session.RemoveSubscription(subscription);
}
catch (Exception ex)
{
Console.WriteLine("OPCUA订阅" + ex.ToString());
}
}
/// <summary>
/// Handle DataChange notifications from Server
/// 订阅后有数据变化回调
/// </summary>
public void OnMonitoredItemNotification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e)
{
try
{
// Log MonitoredItem Notification event
MonitoredItemNotification notification = e.NotificationValue as MonitoredItemNotification;
//if (monitoredItem.DisplayName=="8")
//{
// var irr = notification;
//}
var item = Items.Find(o => o.ItemID == monitoredItem.DisplayName);
if (notification.Value != null)
{
//若是字符串数据的话,则进行直接赋值
if (notification.Value.WrappedValue.TypeInfo.BuiltInType == BuiltInType.String)
{
item.ItemValue = notification.Value.ToString();
}
//在这里进行结构体类型数据处理
//else if (item.ItemType == DataType.Struct)
//{
// item.ItemValue = ReadStruct(item.ItemAdress, notification.Value);
//}
else
{
item.ItemValue = Newtonsoft.Json.JsonConvert.SerializeObject(notification.Value.Value);
}
MessageBox.Show($"数据节点是:{item.ItemAdress} 值是:{item.ItemValue}");
}
//m_output.WriteLine("Notification Received for Variable \"{0}\" and Value = {1}.", monitoredItem.DisplayName, notification.Value);
}
catch (Exception ex)
{
//m_output.WriteLine("OnMonitoredItemNotification error: {0}", ex.Message);
}
}
结构体读写参考我这篇文章C# OPCUA 读写结构体_@榴莲酥的博客-CSDN博客
完成代码
using Newtonsoft.Json;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace opcuaTest
{
/// <summary>
/// opcua工具类
/// </summary>
public class opcuahelper
{
/// <summary>
///
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="m_IP"></param>
/// <param name="m_Port"></param>
public opcuahelper(string username,string password)
{
m_endpoints = new Dictionary<Uri, EndpointDescription>();
if (username != "" && password != "")
{
UserIdentity = new UserIdentity(username, password);
}
Console.WriteLine("加载客户端配置");
#region 加载客户端配置
m_application.ApplicationType = ApplicationType.Client;
m_application.ConfigSectionName = "Treasure.OPCUAClient";
//1.加载配置,如果证书不存在则生成证书
//2.把生成的证书导入到服务端,让服务器信任证书
//3.然后再次连接即可成功
// load the application configuration. 读取XML给加载configuration
m_application.LoadApplicationConfiguration("OPCUAClient.Config.xml", silent: false);
// check the application certificate. 检查证书
m_application.CheckApplicationInstanceCertificate(silent: false, minimumKeySize: 0);
m_configuration = m_application.ApplicationConfiguration;
//当证书验证失败时回调
m_configuration.CertificateValidator.CertificateValidation += CertificateValidation;
#endregion
}
#region Constructors
private ApplicationInstance m_application = new ApplicationInstance();
#endregion
#region Private Fields
private string m_ConnectStr;
private ApplicationConfiguration m_configuration;
private Session m_session;
private SessionReconnectHandler m_reconnectHandler;
private EventHandler m_ReconnectComplete;
private EventHandler m_ReconnectStarting;
private EventHandler m_KeepAliveComplete;
private EventHandler m_ConnectComplete;
private Dictionary<Uri, EndpointDescription> m_endpoints;
#endregion
#region Public Members
/// <summary>
/// Default session values.
/// </summary>
[JsonIgnore]
public static readonly uint DefaultSessionTimeout = 60000;
/// <summary>
/// 默认发现超时
/// </summary>
[JsonIgnore]
public static readonly int DefaultDiscoverTimeout = 15000;
/// <summary>
/// 默认重新连接时间
/// </summary>
[JsonIgnore]
public static readonly int DefaultReconnectPeriod = 1000;
/// <summary>
/// The name of the session to create.
/// </summary>
[JsonIgnore]
public string SessionName { get; set; }
/// <summary>
/// Gets or sets a flag indicating that the domain checks should be ignored when connecting.
/// </summary>
[JsonIgnore]
public bool DisableDomainCheck { get; set; }
/// <summary>
/// Gets the cached EndpointDescription for a Url.
/// </summary>
public EndpointDescription GetEndpointDescription(Uri url)
{
EndpointDescription endpointDescription;
if (m_endpoints.TryGetValue(url, out endpointDescription))
{
return endpointDescription;
}
return null;
}
/// <summary>
/// The URL displayed in the control.
/// </summary>
public string ServerUrl
{
get;
set;
}
/// <summary>
/// Whether to use security when connecting.
/// </summary>
public bool UseSecurity
{
get;
set;
}
/// <summary>
/// Get the Client status.
/// </summary>
public string ConnectStatus
{
get { return m_ConnectStatus; }
}
private string m_ConnectStatus;
/// <summary>
/// The locales to use when creating the session.
/// </summary>
[JsonIgnore]
public string[] PreferredLocales { get; set; }
/// <summary>
/// The user identity to use when creating the session.
/// 认证
/// </summary>
[JsonIgnore]
public IUserIdentity UserIdentity { get; set; } = new UserIdentity();
/// <summary>
/// The client application configuration.
/// 客户端配置,一般用在证书验证中
/// </summary>
[JsonIgnore]
public ApplicationConfiguration Configuration
{
get => m_configuration;
set
{
if (!Object.ReferenceEquals(m_configuration, value))
{
m_configuration = value;
}
}
}
/// <summary>
/// The currently active session.
/// 连接会话
/// </summary>
[JsonIgnore]
public Session Session => m_session;
/// <summary>
/// The number of seconds between reconnect attempts (0 means reconnect is disabled).
/// </summary>
public int ReconnectPeriod { get; set; } = DefaultReconnectPeriod;
/// <summary>
/// The discover timeout.
/// </summary>
public int DiscoverTimeout { get; set; } = DefaultDiscoverTimeout;
/// <summary>
/// The session timeout.
/// </summary>
public uint SessionTimeout { get; set; } = DefaultSessionTimeout;
/// <summary>
/// Raised when a good keep alive from the server arrives.
/// </summary>
public event EventHandler KeepAliveComplete
{
add { m_KeepAliveComplete += value; }
remove { m_KeepAliveComplete -= value; }
}
/// <summary>
/// Raised when a reconnect operation starts.
/// </summary>
public event EventHandler ReconnectStarting
{
add { m_ReconnectStarting += value; }
remove { m_ReconnectStarting -= value; }
}
/// <summary>
/// Raised when a reconnect operation completes.
/// </summary>
public event EventHandler ReconnectComplete
{
add { m_ReconnectComplete += value; }
remove { m_ReconnectComplete -= value; }
}
/// <summary>
/// Raised after successfully connecting to or disconnecing from a server.
/// </summary>
public event EventHandler ConnectComplete
{
add { m_ConnectComplete += value; }
remove { m_ConnectComplete -= value; }
}
/// <summary>
/// Creates a new session.
/// 连接opcua
/// </summary>
/// <returns>The new session object.</returns>
private async Task<Session> Connect(
ITransportWaitingConnection connection,
EndpointDescription endpointDescription,
bool useSecurity,
uint sessionTimeout = 0)
{
// disconnect from existing session.
InternalDisconnect();
// select the best endpoint.
if (endpointDescription == null)
{
endpointDescription = CoreClientUtils.SelectEndpoint(m_configuration, connection, useSecurity, DiscoverTimeout);
}
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration);
ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
m_session = await Session.Create(
m_configuration,
connection,
endpoint,
false,
!DisableDomainCheck,
(String.IsNullOrEmpty(SessionName)) ? m_configuration.ApplicationName : SessionName,
sessionTimeout == 0 ? DefaultSessionTimeout : sessionTimeout,
UserIdentity,
PreferredLocales);
// set up keep alive callback.
m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive);
// return the new session.
return m_session;
}
/// <summary>
/// 连接opcua
/// </summary>
/// <param name="serverUrl">连接地址</param>
/// <param name="useSecurity">是否启用身份验证,true启用,false不启用</param>
/// <param name="sessionTimeout"></param>
/// <returns></returns>
public async Task<Session> ConnectServer(
string serverUrl,
bool useSecurity,
uint sessionTimeout = 0)
{
try
{
// disconnect from existing session.
InternalDisconnect();
// select the best endpoint.
Console.WriteLine("获取远程节点:" + serverUrl);
serverUrl = "opc.tcp://127.0.0.1:49320";
var endpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, useSecurity, 600000);
var endpointConfiguration = EndpointConfiguration.Create(m_configuration);
var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
// Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(endpoint));
Console.WriteLine("开始连接:" + serverUrl);
m_session = await Session.Create(
m_configuration,
endpoint,
false,
DisableDomainCheck,
(String.IsNullOrEmpty(SessionName)) ? m_configuration.ApplicationName : SessionName,
sessionTimeout == 0 ? DefaultSessionTimeout : sessionTimeout,
UserIdentity,
PreferredLocales);
// set up keep alive callback. 监听连接状态,但连接失败时自动重连
m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive);
Console.WriteLine("OPC连接成功:" + serverUrl + "m_session.Connected的连接状态是:" + m_session.Connected);
}
catch (Exception ex)
{
Console.WriteLine("OPC连接异常:" + serverUrl + ex.ToString());
if (m_session != null)
{
m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive);
}
else
{
//isCconnect = false;
}
}
try
{
UpdateStatus(false, DateTime.Now, "Connected, loading complex type system.");
}
catch (Exception e)
{
UpdateStatus(true, DateTime.Now, "Connected, failed to load complex type system.");
Utils.Trace(e, "Failed to load complex type system.");
}
// return the new session.
return m_session;
}
/// <summary>
/// Creates a new session.
/// 连接opcua,异步线程
/// </summary>
/// <param name="serverUrl">The URL of a server endpoint.</param>
/// <param name="useSecurity">Whether to use security.</param>
/// <param name="sessionTimeout">会话超时时间.</param>
/// <returns>The new session object.</returns>
public async Task<Session> ConnectServerAsync(
string serverUrl = null,
bool useSecurity = false,
uint sessionTimeout = 0
)
{
return await Task.Run(() => ConnectServer(serverUrl, useSecurity, sessionTimeout));
}
/// <summary>
/// Create a new reverse connection.
/// </summary>
/// <param name="connection"></param>
/// <param name="useSecurity"></param>
/// <param name="discoverTimeout"></param>
/// <param name="sessionTimeout"></param>
public async Task<Session> ConnectServerAsync(
ITransportWaitingConnection connection,
bool useSecurity,
int discoverTimeout = -1,
uint sessionTimeout = 0
)
{
if (connection.EndpointUrl == null)
{
throw new ArgumentException("Endpoint URL is not valid.");
}
EndpointDescription endpointDescription = null;
if (!m_endpoints.TryGetValue(connection.EndpointUrl, out endpointDescription))
{
// Discovery uses the reverse connection and closes it
// return and wait for next reverse hello
endpointDescription = CoreClientUtils.SelectEndpoint(m_configuration, connection, useSecurity, discoverTimeout);
m_endpoints[connection.EndpointUrl] = endpointDescription;
return null;
}
return await Connect(connection, endpointDescription, useSecurity, sessionTimeout);
}
/// <summary>
/// Disconnects from the server.
/// </summary>
public Task DisconnectAsync()
{
UpdateStatus(false, DateTime.Now, "Disconnected");
return Task.Run(() => InternalDisconnect());
}
/// <summary>
/// 断开连接
/// Disconnects from the server.
/// </summary>
private void InternalDisconnect()
{
// stop any reconnect operation.
if (m_reconnectHandler != null)
{
m_reconnectHandler.Dispose();
m_reconnectHandler = null;
}
// disconnect any existing session.
if (m_session != null)
{
m_session.KeepAlive -= Session_KeepAlive;
m_session.Close(10000);
m_session = null;
}
}
/// <summary>
/// Disconnects from the server.
/// </summary>
public void Disconnect()
{
UpdateStatus(false, DateTime.Now, "Disconnected");
// stop any reconnect operation.
InternalDisconnect();
}
#endregion
#region 订阅的添加、删除、监听
List<itemModel> Items = new List<itemModel>();
Subscription subscription;
/// <summary>
///Create Subscription and MonitoredItems for DataChanges
/// 添加订阅
/// </summary>
/// <param name="interval">采样间隔 单位毫秒</param>
/// <param name="itemBases"></param>
public void Subscribe(int interval, List<itemModel> itemBases)
{
try
{
// Create a subscription for receiving data change notifications
// Define Subscription parameters
subscription = new Subscription(m_session.DefaultSubscription);
subscription.DisplayName = "OPCUA Subscribe";
subscription.PublishingEnabled = true;
subscription.PublishingInterval = 200;
for (int i = 0; i < itemBases.Count; i++)
{
// Create MonitoredItems for data changes
MonitoredItem MonitoredItem = new MonitoredItem(subscription.DefaultItem);
// Int32 Node - Objects\CTT\Scalar\Simulation\Int32
MonitoredItem.StartNodeId = new NodeId(itemBases[i].ItemAdress);
MonitoredItem.AttributeId = Attributes.Value;
MonitoredItem.DisplayName = itemBases[i].ItemID;
MonitoredItem.SamplingInterval = interval;
MonitoredItem.Notification += OnMonitoredItemNotification;
subscription.AddItem(MonitoredItem);
Items.Add(itemBases[i]);
}
m_session.AddSubscription(subscription);
// Create the subscription on Server side
subscription.Create();
// Create the monitored items on Server side
//subscription.ApplyChanges();
}
catch (Exception ex)
{
Console.WriteLine("OPCUA订阅" + ex.ToString());
}
}
/// <summary>
/// Create Subscription and MonitoredItems for DataChanges
/// 移除订阅
/// </summary>
public void RemoveSubscribe()
{
try
{
m_session.RemoveSubscription(subscription);
}
catch (Exception ex)
{
Console.WriteLine("OPCUA订阅" + ex.ToString());
}
}
/// <summary>
/// Handle DataChange notifications from Server
/// 订阅后有数据变化回调
/// </summary>
public void OnMonitoredItemNotification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e)
{
try
{
// Log MonitoredItem Notification event
MonitoredItemNotification notification = e.NotificationValue as MonitoredItemNotification;
//if (monitoredItem.DisplayName=="8")
//{
// var irr = notification;
//}
var item = Items.Find(o => o.ItemID == monitoredItem.DisplayName);
if (notification.Value != null)
{
//若是字符串数据的话,则进行直接赋值
if (notification.Value.WrappedValue.TypeInfo.BuiltInType == BuiltInType.String)
{
item.ItemValue = notification.Value.ToString();
}
//在这里进行结构体类型数据处理
//else if (item.ItemType == DataType.Struct)
//{
// item.ItemValue = ReadStruct(item.ItemAdress, notification.Value);
//}
else
{
item.ItemValue = Newtonsoft.Json.JsonConvert.SerializeObject(notification.Value.Value);
}
MessageBox.Show($"数据节点是:{item.ItemAdress} 值是:{item.ItemValue}");
}
//m_output.WriteLine("Notification Received for Variable \"{0}\" and Value = {1}.", monitoredItem.DisplayName, notification.Value);
}
catch (Exception ex)
{
//m_output.WriteLine("OnMonitoredItemNotification error: {0}", ex.Message);
}
}
#endregion
#region Event Handlers
/// <summary>
/// Updates the status control.
/// </summary>
/// <param name="error">Whether the status represents an error.</param>
/// <param name="time">The time associated with the status.</param>
/// <param name="status">The status message.</param>
/// <param name="args">Arguments used to format the status message.</param>
private void UpdateStatus(bool error, DateTime time, string status, params object[] args)
{
m_ConnectStatus = String.Format(status, args);
}
/// <summary>
/// Handles a keep alive event from a session.
/// 监听连接状态,当连接失败时自动重连
/// </summary>
private void Session_KeepAlive(Session session, KeepAliveEventArgs e)
{
try
{
// check for events from discarded sessions.
if (!Object.ReferenceEquals(session, m_session))
{
return;
}
// start reconnect sequence on communication error.
if (Opc.Ua.ServiceResult.IsBad(e.Status))
{
if (ReconnectPeriod <= 0)
{
UpdateStatus(true, e.CurrentTime, "Communication Error ({0})", e.Status);
return;
}
UpdateStatus(true, e.CurrentTime, "Reconnecting in {0}s", ReconnectPeriod);
if (m_reconnectHandler == null)
{
if (m_ReconnectStarting != null)
{
m_ReconnectStarting(this, e);
}
m_reconnectHandler = new SessionReconnectHandler();
//重连
m_reconnectHandler.BeginReconnect(m_session, ReconnectPeriod, Server_ReconnectComplete);
}
return;
}
// update status.
UpdateStatus(false, e.CurrentTime, "Connected [{0}]", session.Endpoint.EndpointUrl);
// raise any additional notifications.
if (m_KeepAliveComplete != null)
{
m_KeepAliveComplete(this, e);
}
}
catch (Exception exception)
{
throw new Exception("Session_KeepAlive", exception);
}
}
/// <summary>
/// Handles a reconnect event complete from the reconnect handler.
/// 连接断开重连成功后执行该方法
/// </summary>
private void Server_ReconnectComplete(object sender, EventArgs e)
{
try
{
// ignore callbacks from discarded objects.
if (!Object.ReferenceEquals(sender, m_reconnectHandler))
{
return;
}
m_session = m_reconnectHandler.Session;
m_reconnectHandler.Dispose();
m_reconnectHandler = null;
// raise any additional notifications.
if (m_ReconnectComplete != null)
{
m_ReconnectComplete(this, e);
}
//isCconnect = true;
//if (!isSub)
//{
// ReadItems();
//}
}
catch (Exception exception)
{
throw new Exception("Server_ReconnectComplete", exception);
}
}
#endregion
#region 读写数据
/// <summary>
/// OPCUA批量Tag读取
/// 把读取的数据值都转成string
/// </summary>
/// <param name="itemBases">需要读取的节点</param>
/// <returns></returns>
public void ReadNodes(List<itemModel> itemBases)
{
try
{
if (m_session.Connected)
{
//Console.WriteLine("数据节点转换");
//节点格式转换
ReadValueIdCollection nodesToRead = new ReadValueIdCollection();
for (int i = 0; i < itemBases.Count; i++)
{
nodesToRead.Add(new ReadValueId()
{
NodeId = new NodeId(itemBases[i].ItemAdress),
AttributeId = Attributes.Value
});
}
NodeId ss = itemBases[0].ItemAdress;
//EncodeableFactory.GlobalFactory.AddEncodeableType(typeof(QTI_Data));
// Read the node attributes
DataValueCollection resultsValues = null;
DiagnosticInfoCollection diagnosticInfos = null;
//Console.WriteLine("数据读取开始");
// Call Read Service
m_session.Read(
null,
0,
TimestampsToReturn.Both,
nodesToRead,
out resultsValues,
out diagnosticInfos);
//验证结果
ClientBase.ValidateResponse(resultsValues, nodesToRead);
for (int i = 0; i < resultsValues.Count; i++)
{
//这里数组还有其他的标准数据类型都当转string赋值,当然你也可以根据resultsValues[i].WrappedValue.TypeInfo一个个处理
if (resultsValues[i].Value == null)
{
itemBases[i].ItemValue = "";
}
else
{
//若是字符串数据的话,则进行直接赋值
if (resultsValues[i].WrappedValue.TypeInfo.BuiltInType == BuiltInType.String)
{
itemBases[i].ItemValue = resultsValues[i].Value.ToString();
}
else
{
itemBases[i].ItemValue = Newtonsoft.Json.JsonConvert.SerializeObject(resultsValues[i].Value);
}
}
//在这里进行结构体类型数据处理
if (itemBases[i].ItemType == DataType.Struct)
{
// itemBases[i].ItemValue = ReadStruct(itemBases[i].ItemAdress, resultsValues[i]);
}
}
}
}
catch (Exception ex)
{
throw new Exception("OPUAReadNodes:" + ex.Message + ex.StackTrace);
}
}
/// <summary>
/// 单个写入节点,注意T的数据类型要与OPCUA服务器的数据类型相对应
/// 返回true写入成功,false失败
/// </summary>
/// <typeparam name="T">写入的数据类型</typeparam>
/// <param name="t">写入的值</param>
/// <param name="nodeAddress">写入值的地址</param>
public bool WriteNodes<T>(T t, string nodeAddress)
{
try
{
if (m_session.Connected)
{
//ApplicationLog.operateLog("数据节点转换");
//节点格式转换
WriteValueCollection nodesToWrite = new WriteValueCollection();
//if (itemBases.ItemType == DataType.Struct)//类型是结构体时
//{
// List<WriteVar> WriteVarList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<WriteVar>>(itemBases.ItemValue);
// ExtensionObject[] ExtensionObjectArr = new ExtensionObject[100];
// for (int j = 0; j < ExtensionObjectArr.Length; j++)
// {
// ExtensionObjectArr[j] = new ExtensionObject { Body = getNewByte() };
// }
// foreach (var item in WriteVarList)
// {
// var result1 = GetValueByteFromObj(item);
// ExtensionObjectArr[Convert.ToInt32(item.index)] = result1;
// }
// nodesToWrite.Add(new WriteValue()
// {
// NodeId = new NodeId(itemBases.ItemAdress),
// AttributeId = Attributes.Value,
// Value = new DataValue()
// {
// Value = ExtensionObjectArr
// }
// });
//}
nodesToWrite.Add(new WriteValue()
{
NodeId = new NodeId(nodeAddress),
AttributeId = Attributes.Value,
Value = new DataValue()
{
Value = t
}
});
//NodeId ss = itemBases.ItemAdress;
//EncodeableFactory.GlobalFactory.AddEncodeableType(typeof(QTI_Data));
// Read the node attributes
StatusCodeCollection resultsValues = null;
DiagnosticInfoCollection diagnosticInfos = null;
//ApplicationLog.operateLog("数据读取开始");
// Call Read Service
m_session.Write(
null,
nodesToWrite,
out resultsValues,
out diagnosticInfos);
//验证结果
ClientBase.ValidateResponse(resultsValues, nodesToWrite);
bool result = true;
foreach (var r in resultsValues)
{
if (StatusCode.IsBad(r))
{
result = false;
break;
}
}
return result;
}
else
{
throw new Exception("OPCUA连接断开");
}
}
catch (Exception ex)
{
Console.WriteLine("WriteNodes 写入异常" + ex.ToString());
throw;
}
}
#endregion
#region 认证
/// <summary>
/// Handles the certificate validation event.当证书验证失败时回调事件
/// This event is triggered every time an untrusted certificate is received from the server.
/// </summary>
private void CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e)
{
bool certificateAccepted = true;
// ****
// Implement a custom logic to decide if the certificate should be
// accepted or not and set certificateAccepted flag accordingly.
// The certificate can be retrieved from the e.Certificate field
// ***
ServiceResult error = e.Error;
while (error != null)
{
Console.WriteLine(error);
error = error.InnerResult;
}
if (certificateAccepted)
{
Console.WriteLine("Untrusted Certificate accepted. SubjectName = {0}", e.Certificate.SubjectName);
}
e.AcceptAll = certificateAccepted;
}
#endregion
}
}
6.demo
所有demo和资料都打包放在一起,大家可以一起下载使用。下面截图时opcua运行所需DLL