Files
KinginfoGateway/framework/Foundation/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient/OPCUAClient.cs
2023-10-04 17:12:47 +08:00

1697 lines
59 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#region copyright
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://diego2098.gitee.io/thingsgateway-docs/
// QQ群605534569
//------------------------------------------------------------------------------
#endregion
using Newtonsoft.Json.Linq;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.ComplexTypes;
using Opc.Ua.Configuration;
//修改自https://github.com/dathlin/OpcUaHelper 与OPC基金会net库
namespace ThingsGateway.Foundation.Adapter.OPCUA;
/// <summary>
/// 订阅委托
/// </summary>
/// <param name="value"></param>
public delegate void DataChangedEventHandler((VariableNode variableNode, DataValue dataValue, JToken jToken) value);
/// <summary>
/// OPCUAClient
/// </summary>
public class OPCUAClient : IDisposable
{
#region
/// <summary>
/// 当前配置
/// </summary>
public OPCNode OPCNode;
/// <summary>
/// ProductUri
/// </summary>
public string ProductUri = "https://diego2098.gitee.io/thingsgateway-docs/";
/// <summary>
/// 当前保存的变量名称列表
/// </summary>
public List<string> Variables = new();
private readonly Action<byte, object, string, Exception> _logAction;
/// <summary>
/// 当前的变量名称/OPC变量节点
/// </summary>
private readonly Dictionary<string, VariableNode> _variableDicts = new();
private readonly object checkLock = new();
/// <summary>
/// 当前的订阅组,组名称/组
/// </summary>
private readonly Dictionary<string, Subscription> dic_subscriptions = new();
private readonly ApplicationInstance m_application = new();
private readonly ApplicationConfiguration m_configuration;
private SessionReconnectHandler m_reConnectHandler;
private ISession m_session;
/// <summary>
/// 默认的构造函数实例化一个新的OPC UA类
/// </summary>
public OPCUAClient(Action<byte, object, string, Exception> log)
{
_logAction = log;
var certificateValidator = new CertificateValidator();
certificateValidator.CertificateValidation += CertificateValidation;
// 构建应用程序配置
m_configuration = new ApplicationConfiguration
{
ApplicationName = OPCUAName,
ApplicationType = ApplicationType.Client,
CertificateValidator = certificateValidator,
ApplicationUri = Utils.Format(@"urn:{0}:{1}", System.Net.Dns.GetHostName(), OPCUAName),
ProductUri = ProductUri,
ServerConfiguration = new ServerConfiguration
{
MaxSubscriptionCount = 100000,
MaxMessageQueueSize = 1000000,
MaxNotificationQueueSize = 1000000,
MaxPublishRequestCount = 10000000,
},
SecurityConfiguration = new SecurityConfiguration
{
UseValidatedCertificates = true,
AutoAcceptUntrustedCertificates = true,//自动接受证书
RejectSHA1SignedCertificates = false,
MinimumCertificateKeySize = 1024,
SuppressNonceValidationErrors = true,
ApplicationCertificate = new CertificateIdentifier
{
StoreType = CertificateStoreType.X509Store,
StorePath = "CurrentUser\\" + OPCUAName,
SubjectName = $"CN={OPCUAName}, C=CN, S=GUANGZHOU, O=ThingsGateway, DC=" + System.Net.Dns.GetHostName(),
},
TrustedIssuerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = AppContext.BaseDirectory + @"OPCUAClientCertificate\pki\trustedIssuer",
},
TrustedPeerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = AppContext.BaseDirectory + @"OPCUAClientCertificate\pki\trustedPeer",
},
RejectedCertificateStore = new CertificateStoreIdentifier
{
StoreType = CertificateStoreType.Directory,
StorePath = AppContext.BaseDirectory + @"OPCUAClientCertificate\pki\rejected",
},
UserIssuerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = AppContext.BaseDirectory + @"OPCUAClientCertificate\pki\issuerUser",
},
TrustedUserCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = AppContext.BaseDirectory + @"OPCUAClientCertificate\pki\trustedUser",
}
},
TransportQuotas = new TransportQuotas
{
OperationTimeout = 6000000,
MaxStringLength = int.MaxValue,
MaxByteStringLength = int.MaxValue,
MaxArrayLength = 65535,
MaxMessageSize = 419430400,
MaxBufferSize = 65535,
ChannelLifetime = -1,
SecurityTokenLifetime = -1
},
ClientConfiguration = new ClientConfiguration
{
DefaultSessionTimeout = -1,
MinSubscriptionLifetime = -1,
},
DisableHiResClock = true
};
certificateValidator.Update(m_configuration);
m_configuration.Validate(ApplicationType.Client);
m_application.ApplicationConfiguration = m_configuration;
}
/// <summary>
/// 订阅
/// </summary>
public event DataChangedEventHandler DataChangedHandler;
/// <summary>
/// 配置信息
/// </summary>
public ApplicationConfiguration AppConfig => m_configuration;
/// <summary>
/// 连接状态
/// </summary>
public bool Connected => m_session?.Connected == true;
/// <summary>
/// OPCUAClient
/// </summary>
public string OPCUAName { get; set; } = "ThingsGateway";
/// <summary>
/// SessionReconnectHandler
/// </summary>
public SessionReconnectHandler ReConnectHandler => m_reConnectHandler;
/// <summary>
/// 当前活动会话。
/// </summary>
public ISession Session => m_session;
#endregion
#region
/// <summary>
/// 新增订阅需要指定订阅组名称订阅的tag名数组
/// </summary>
public async Task AddSubscriptionAsync(string subscriptionName, string[] items)
{
Subscription m_subscription = new(m_session.DefaultSubscription)
{
PublishingEnabled = true,
PublishingInterval = 0,
KeepAliveCount = uint.MaxValue,
LifetimeCount = uint.MaxValue,
MaxNotificationsPerPublish = uint.MaxValue,
Priority = 100,
DisplayName = subscriptionName
};
List<MonitoredItem> monitoredItems = new();
for (int i = 0; i < items.Length; i++)
{
try
{
var variableNode = await ReadNodeAsync(items[i], false);
var item = new MonitoredItem
{
StartNodeId = variableNode.NodeId,
AttributeId = Attributes.Value,
DisplayName = items[i],
Filter = OPCNode.DeadBand == 0 ? null : new DataChangeFilter() { DeadbandValue = OPCNode.DeadBand, DeadbandType = (int)DeadbandType.Absolute, Trigger = DataChangeTrigger.StatusValue },
SamplingInterval = OPCNode?.UpdateRate ?? 1000,
};
item.Notification += Callback;
monitoredItems.Add(item);
}
catch (Exception ex)
{
_logAction?.Invoke(3, this, $"初始化{items[i]}变量订阅失败", ex);
}
}
m_subscription.AddItems(monitoredItems);
m_session.AddSubscription(m_subscription);
m_subscription.Create();
foreach (var item in m_subscription.MonitoredItems.Where(a => a.Status.Error != null && StatusCode.IsBad(a.Status.Error.StatusCode)))
{
item.Filter = OPCNode.DeadBand == 0 ? null : new DataChangeFilter() { DeadbandValue = OPCNode.DeadBand, DeadbandType = (int)DeadbandType.None, Trigger = DataChangeTrigger.StatusValue };
}
m_subscription.ApplyChanges();
var isError = m_subscription.MonitoredItems.Any(a => a.Status.Error != null && StatusCode.IsBad(a.Status.Error.StatusCode));
if (isError)
{
_logAction?.Invoke(3, this, $"创建以下变量订阅失败:{Environment.NewLine}{m_subscription.MonitoredItems.Where(
a => a.Status.Error != null && StatusCode.IsBad(a.Status.Error.StatusCode))
.Select(a => a.StartNodeId.ToString() + "" + a.Status.Error.ToString()).ToJsonString()}", null);
}
lock (dic_subscriptions)
{
if (dic_subscriptions.ContainsKey(subscriptionName))
{
// remove
dic_subscriptions[subscriptionName].Delete(true);
m_session.RemoveSubscription(dic_subscriptions[subscriptionName]);
try { dic_subscriptions[subscriptionName].Dispose(); } catch { }
dic_subscriptions[subscriptionName] = m_subscription;
}
else
{
dic_subscriptions.Add(subscriptionName, m_subscription);
}
}
}
/// <summary>
/// 移除所有的订阅消息
/// </summary>
public void RemoveAllSubscription()
{
lock (dic_subscriptions)
{
foreach (var item in dic_subscriptions)
{
item.Value.Delete(true);
m_session.RemoveSubscription(item.Value);
try { item.Value.Dispose(); } catch { }
}
dic_subscriptions.Clear();
}
}
/// <summary>
/// 移除订阅消息
/// </summary>
/// <param name="subscriptionName">组名称</param>
public void RemoveSubscription(string subscriptionName)
{
lock (dic_subscriptions)
{
if (dic_subscriptions.ContainsKey(subscriptionName))
{
// remove
dic_subscriptions[subscriptionName].Delete(true);
m_session.RemoveSubscription(dic_subscriptions[subscriptionName]);
try { dic_subscriptions[subscriptionName].Dispose(); } catch { }
dic_subscriptions.RemoveWhere(a => a.Key == subscriptionName);
}
}
}
private async void Callback(MonitoredItem monitoreditem, MonitoredItemNotificationEventArgs monitoredItemNotificationEventArgs)
{
try
{
var variableNode = await ReadNodeAsync(monitoreditem.StartNodeId.ToString(), false);
foreach (var value in monitoreditem.DequeueValues())
{
if (value.Value != null)
{
var data = JsonUtils.Encode(m_session.MessageContext, TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable), value.Value);
if (data == null && value.Value != null)
{
_logAction?.Invoke(3, this, $"{monitoreditem.StartNodeId}转换出错原始值String为{value.Value}", null);
var data1 = JsonUtils.Encode(m_session.MessageContext, TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable), value.Value);
}
DataChangedHandler?.Invoke((variableNode, value, data));
}
else
{
var data = JValue.CreateNull();
DataChangedHandler?.Invoke((variableNode, value, data));
}
}
}
catch (Exception ex)
{
_logAction?.Invoke(3, this, $"{monitoreditem.StartNodeId}订阅处理错误", ex);
}
}
#endregion
#region
/// <summary>
/// 浏览一个节点的引用
/// </summary>
/// <param name="tag">节点值</param>
/// <returns>引用节点描述</returns>
public async Task<ReferenceDescription[]> BrowseNodeReferenceAsync(string tag)
{
NodeId sourceId = new(tag);
// 该节点可以读取到方法
BrowseDescription nodeToBrowse1 = new()
{
NodeId = sourceId,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.Aggregates,
IncludeSubtypes = true,
NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable | NodeClass.Method),
ResultMask = (uint)BrowseResultMask.All
};
// find all nodes organized by the node.
BrowseDescription nodeToBrowse2 = new()
{
NodeId = sourceId,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.Organizes,
IncludeSubtypes = true,
NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable),
ResultMask = (uint)BrowseResultMask.All
};
BrowseDescriptionCollection nodesToBrowse = new()
{
nodeToBrowse1,
nodeToBrowse2
};
// fetch references from the server.
ReferenceDescriptionCollection references = await FormUtils.BrowseAsync(m_session, nodesToBrowse, false);
return references.ToArray();
}
/// <summary>
/// 调用服务器的方法
/// </summary>
/// <param name="tagParent">方法的父节点tag</param>
/// <param name="tag">方法的节点tag</param>
/// <param name="args">传递的参数</param>
/// <returns>输出的结果值</returns>
public object[] CallMethodByNodeId(string tagParent, string tag, params object[] args)
{
if (m_session == null)
{
return null;
}
IList<object> outputArguments = m_session.Call(
new NodeId(tagParent),
new NodeId(tag),
args);
return outputArguments.ToArray();
}
/// <summary>
/// 读取历史数据
/// </summary>
/// <param name="tag">节点的索引</param>
/// <param name="start">开始时间</param>
/// <param name="end">结束时间</param>
/// <param name="count">读取的个数</param>
/// <param name="containBound">是否包含边界</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns>读取的数据列表</returns>
public async Task<List<DataValue>> ReadHistoryRawDataValues(string tag, DateTime start, DateTime end, uint count = 1, bool containBound = false, CancellationToken cancellationToken = default)
{
HistoryReadValueId m_nodeToContinue = new()
{
NodeId = new NodeId(tag),
};
ReadRawModifiedDetails m_details = new()
{
StartTime = start,
EndTime = end,
NumValuesPerNode = count,
IsReadModified = false,
ReturnBounds = containBound
};
HistoryReadValueIdCollection nodesToRead = new()
{
m_nodeToContinue
};
var result = await m_session.HistoryReadAsync(
null,
new ExtensionObject(m_details),
TimestampsToReturn.Both,
false,
nodesToRead,
cancellationToken);
var results = result.Results;
var diagnosticInfos = result.DiagnosticInfos;
ClientBase.ValidateResponse(results, nodesToRead);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);
if (StatusCode.IsBad(results[0].StatusCode))
{
throw new ServiceResultException(results[0].StatusCode);
}
HistoryData values = ExtensionObject.ToEncodeable(results[0].HistoryData) as HistoryData;
return values.DataValues;
}
#endregion
#region
private ComplexTypeSystem typeSystem;
/// <summary>
/// 连接到服务器
/// </summary>
public async Task ConnectAsync()
{
await ConnectAsync(OPCNode.OPCUrl);
_logAction?.Invoke(1, this, $"连接成功", null);
}
/// <summary>
/// 断开连接。
/// </summary>
public void Disconnect()
{
PrivateDisconnect();
// disconnect any existing session.
if (m_session != null)
{
_logAction?.Invoke(1, this, $"主动断开连接", null);
m_session = null;
}
}
/// <summary>
/// Creates a new session.
/// </summary>
/// <returns>The new session object.</returns>
private async Task<ISession> ConnectAsync(string serverUrl)
{
PrivateDisconnect();
if (m_configuration == null)
{
throw new ArgumentNullException("未初始化配置");
}
var useSecurity = OPCNode?.IsUseSecurity ?? true;
EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(m_configuration, serverUrl, useSecurity, 10000);
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration);
ConfiguredEndpoint endpoint = new(null, endpointDescription, endpointConfiguration);
UserIdentity userIdentity;
if (!string.IsNullOrEmpty(OPCNode.UserName))
{
userIdentity = new UserIdentity(OPCNode.UserName, OPCNode.Password);
}
else
{
userIdentity = new UserIdentity(new AnonymousIdentityToken());
}
//创建本地证书
await m_application.CheckApplicationInstanceCertificate(true, 0, 1200);
m_session = await Opc.Ua.Client.Session.Create(
m_configuration,
endpoint,
false,
OPCNode.CheckDomain,
(string.IsNullOrEmpty(OPCUAName)) ? m_configuration.ApplicationName : OPCUAName,
60000,
userIdentity,
Array.Empty<string>());
typeSystem = new ComplexTypeSystem(m_session);
m_session.KeepAliveInterval = OPCNode.KeepAliveInterval == 0 ? 60000 : OPCNode.KeepAliveInterval;
m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive);
//如果是订阅模式,连接时添加订阅组
if (OPCNode.ActiveSubscribe)
await AddSubscriptionAsync(Guid.NewGuid().ToString(), Variables.ToArray());
return m_session;
}
private void PrivateDisconnect()
{
if (m_reConnectHandler != null)
{
try { m_reConnectHandler.Dispose(); } catch { }
m_reConnectHandler = null;
}
if (m_session != null)
{
m_session.KeepAlive -= Session_KeepAlive;
m_session.Close(10000);
}
}
#endregion
#region /
/// <summary>
/// 从服务器读取值
/// </summary>
public async Task<List<(string, DataValue, JToken)>> ReadJTokenValueAsync(string[] tags, CancellationToken cancellationToken = default)
{
var result = await ReadJTokenValueAsync(tags.Select(a => new NodeId(a)).ToArray(), cancellationToken);
return result;
}
/// <summary>
/// 异步写opc标签
/// </summary>
public async Task<Dictionary<string, Tuple<bool, string>>> WriteNodeAsync(Dictionary<string, JToken> writeInfoLists, CancellationToken cancellationToken = default)
{
Dictionary<string, Tuple<bool, string>> results = new();
try
{
WriteValueCollection valuesToWrite = new();
foreach (var item in writeInfoLists)
{
WriteValue valueToWrite = new()
{
NodeId = new NodeId(item.Key),
AttributeId = Attributes.Value,
};
var variableNode = await ReadNodeAsync(item.Key, false, cancellationToken);
var dataValue = JsonUtils.Decode(
m_session.MessageContext,
variableNode.DataType,
TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable),
item.Value.CalculateActualValueRank(),
item.Value
);
valueToWrite.Value = dataValue;
valuesToWrite.Add(valueToWrite);
}
var result = await m_session.WriteAsync(
requestHeader: null,
nodesToWrite: valuesToWrite, cancellationToken);
ClientBase.ValidateResponse(result.Results, valuesToWrite);
ClientBase.ValidateDiagnosticInfos(result.DiagnosticInfos, valuesToWrite);
var keys = writeInfoLists.Keys.ToList();
for (int i = 0; i < keys.Count; i++)
{
if (!StatusCode.IsGood(result.Results[i]))
results.Add(keys[i], Tuple.Create(true, result.Results[i].ToString()));
else
{
results.Add(keys[i], Tuple.Create(false, "成功"));
}
}
return results;
}
catch (Exception ex)
{
var keys = writeInfoLists.Keys.ToList();
foreach (var item in keys)
{
results.Add(item, Tuple.Create(true, ex.Message));
}
return results;
}
}
/// <summary>
/// 从服务器读取值
/// </summary>
private async Task<List<(string, DataValue, JToken)>> ReadJTokenValueAsync(NodeId[] nodeIds, CancellationToken cancellationToken = default)
{
if (m_session == null)
{
throw new("服务器未初始化连接");
}
ReadValueIdCollection nodesToRead = new();
for (int i = 0; i < nodeIds.Length; i++)
{
nodesToRead.Add(new ReadValueId()
{
NodeId = nodeIds[i],
AttributeId = Attributes.Value
});
}
// 读取当前的值
var result = await m_session.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
nodesToRead,
cancellationToken);
var results = result.Results;
var diagnosticInfos = result.DiagnosticInfos;
ClientBase.ValidateResponse(results, nodesToRead);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);
List<(string, DataValue, JToken)> jTokens = new();
for (int i = 0; i < results.Count; i++)
{
var variableNode = await ReadNodeAsync(nodeIds[i].ToString(), false, cancellationToken);
var type = TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable);
var jToken = JsonUtils.Encode(m_session.MessageContext, type, results[i].Value);
jTokens.Add((variableNode.NodeId.ToString(), results[i], jToken));
}
return jTokens.ToList();
}
/// <summary>
/// 从服务器或缓存读取节点
/// </summary>
private async Task<VariableNode> ReadNodeAsync(string nodeIdStr, bool isOnlyServer = true, CancellationToken cancellationToken = default)
{
if (!isOnlyServer)
{
if (_variableDicts.TryGetValue(nodeIdStr, out var value))
{
return value;
}
}
NodeId nodeToRead = new(nodeIdStr);
var node = (VariableNode)await ReadNodeAsync(nodeToRead, NodeClass.Unspecified, false, cancellationToken);
await typeSystem.LoadType(node.DataType, true, false);
_variableDicts.AddOrUpdate(nodeIdStr, node);
return node;
}
#endregion
#region session
/// <inheritdoc/>
public async Task<Node> ReadNodeAsync(
NodeId nodeId,
NodeClass nodeClass,
bool optionalAttributes = true,
CancellationToken ct = default)
{
// build list of attributes.
var attributes = CreateAttributes(nodeClass, optionalAttributes);
// build list of values to read.
ReadValueIdCollection itemsToRead = new ReadValueIdCollection();
foreach (uint attributeId in attributes.Keys)
{
ReadValueId itemToRead = new ReadValueId
{
NodeId = nodeId,
AttributeId = attributeId
};
itemsToRead.Add(itemToRead);
}
// read from server.
ReadResponse readResponse = await m_session.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
itemsToRead, ct).ConfigureAwait(false);
DataValueCollection values = readResponse.Results;
DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos;
ClientBase.ValidateResponse(values, itemsToRead);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead);
return ProcessReadResponse(readResponse.ResponseHeader, attributes, itemsToRead, values, diagnosticInfos);
}
/// <summary>
/// Creates a Node based on the read response.
/// </summary>
private Node ProcessReadResponse(
ResponseHeader responseHeader,
IDictionary<uint, DataValue> attributes,
ReadValueIdCollection itemsToRead,
DataValueCollection values,
DiagnosticInfoCollection diagnosticInfos)
{
// process results.
int? nodeClass = null;
for (int ii = 0; ii < itemsToRead.Count; ii++)
{
uint attributeId = itemsToRead[ii].AttributeId;
// the node probably does not exist if the node class is not found.
if (attributeId == Attributes.NodeClass)
{
if (!DataValue.IsGood(values[ii]))
{
throw ServiceResultException.Create(values[ii].StatusCode, ii, diagnosticInfos, responseHeader.StringTable);
}
// check for valid node class.
nodeClass = values[ii].Value as int?;
if (nodeClass == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not have a valid value for NodeClass: {0}.", values[ii].Value);
}
}
else
{
if (!DataValue.IsGood(values[ii]))
{
// check for unsupported attributes.
if (values[ii].StatusCode == StatusCodes.BadAttributeIdInvalid)
{
continue;
}
// ignore errors on optional attributes
if (StatusCode.IsBad(values[ii].StatusCode))
{
if (attributeId == Attributes.AccessRestrictions ||
attributeId == Attributes.Description ||
attributeId == Attributes.RolePermissions ||
attributeId == Attributes.UserRolePermissions ||
attributeId == Attributes.DataTypeDefinition ||
attributeId == Attributes.AccessLevelEx ||
attributeId == Attributes.UserWriteMask ||
attributeId == Attributes.WriteMask)
{
continue;
}
}
// all supported attributes must be readable.
if (attributeId != Attributes.Value)
{
throw ServiceResultException.Create(values[ii].StatusCode, ii, diagnosticInfos, responseHeader.StringTable);
}
}
}
attributes[attributeId] = values[ii];
}
Node node;
DataValue value;
switch ((NodeClass)nodeClass.Value)
{
default:
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not have a valid value for NodeClass: {0}.", nodeClass.Value);
}
case NodeClass.Object:
{
ObjectNode objectNode = new ObjectNode();
value = attributes[Attributes.EventNotifier];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Object does not support the EventNotifier attribute.");
}
objectNode.EventNotifier = (byte)value.GetValue(typeof(byte));
node = objectNode;
break;
}
case NodeClass.ObjectType:
{
ObjectTypeNode objectTypeNode = new ObjectTypeNode();
value = attributes[Attributes.IsAbstract];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "ObjectType does not support the IsAbstract attribute.");
}
objectTypeNode.IsAbstract = (bool)value.GetValue(typeof(bool));
node = objectTypeNode;
break;
}
case NodeClass.Variable:
{
VariableNode variableNode = new VariableNode();
// DataType Attribute
value = attributes[Attributes.DataType];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the DataType attribute.");
}
variableNode.DataType = (NodeId)value.GetValue(typeof(NodeId));
// ValueRank Attribute
value = attributes[Attributes.ValueRank];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the ValueRank attribute.");
}
variableNode.ValueRank = (int)value.GetValue(typeof(int));
// ArrayDimensions Attribute
value = attributes[Attributes.ArrayDimensions];
if (value != null)
{
if (value.Value == null)
{
variableNode.ArrayDimensions = Array.Empty<uint>();
}
else
{
variableNode.ArrayDimensions = (uint[])value.GetValue(typeof(uint[]));
}
}
// AccessLevel Attribute
value = attributes[Attributes.AccessLevel];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the AccessLevel attribute.");
}
variableNode.AccessLevel = (byte)value.GetValue(typeof(byte));
// UserAccessLevel Attribute
value = attributes[Attributes.UserAccessLevel];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the UserAccessLevel attribute.");
}
variableNode.UserAccessLevel = (byte)value.GetValue(typeof(byte));
// Historizing Attribute
value = attributes[Attributes.Historizing];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the Historizing attribute.");
}
variableNode.Historizing = (bool)value.GetValue(typeof(bool));
// MinimumSamplingInterval Attribute
value = attributes[Attributes.MinimumSamplingInterval];
if (value != null)
{
variableNode.MinimumSamplingInterval = Convert.ToDouble(attributes[Attributes.MinimumSamplingInterval].Value);
}
// AccessLevelEx Attribute
value = attributes[Attributes.AccessLevelEx];
if (value != null)
{
variableNode.AccessLevelEx = (uint)value.GetValue(typeof(uint));
}
node = variableNode;
break;
}
case NodeClass.VariableType:
{
VariableTypeNode variableTypeNode = new VariableTypeNode();
// IsAbstract Attribute
value = attributes[Attributes.IsAbstract];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "VariableType does not support the IsAbstract attribute.");
}
variableTypeNode.IsAbstract = (bool)value.GetValue(typeof(bool));
// DataType Attribute
value = attributes[Attributes.DataType];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "VariableType does not support the DataType attribute.");
}
variableTypeNode.DataType = (NodeId)value.GetValue(typeof(NodeId));
// ValueRank Attribute
value = attributes[Attributes.ValueRank];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "VariableType does not support the ValueRank attribute.");
}
variableTypeNode.ValueRank = (int)value.GetValue(typeof(int));
// ArrayDimensions Attribute
value = attributes[Attributes.ArrayDimensions];
if (value != null && value.Value != null)
{
variableTypeNode.ArrayDimensions = (uint[])value.GetValue(typeof(uint[]));
}
node = variableTypeNode;
break;
}
case NodeClass.Method:
{
MethodNode methodNode = new MethodNode();
// Executable Attribute
value = attributes[Attributes.Executable];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Method does not support the Executable attribute.");
}
methodNode.Executable = (bool)value.GetValue(typeof(bool));
// UserExecutable Attribute
value = attributes[Attributes.UserExecutable];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Method does not support the UserExecutable attribute.");
}
methodNode.UserExecutable = (bool)value.GetValue(typeof(bool));
node = methodNode;
break;
}
case NodeClass.DataType:
{
DataTypeNode dataTypeNode = new DataTypeNode();
// IsAbstract Attribute
value = attributes[Attributes.IsAbstract];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "DataType does not support the IsAbstract attribute.");
}
dataTypeNode.IsAbstract = (bool)value.GetValue(typeof(bool));
// DataTypeDefinition Attribute
value = attributes[Attributes.DataTypeDefinition];
if (value != null)
{
dataTypeNode.DataTypeDefinition = value.Value as ExtensionObject;
}
node = dataTypeNode;
break;
}
case NodeClass.ReferenceType:
{
ReferenceTypeNode referenceTypeNode = new ReferenceTypeNode();
// IsAbstract Attribute
value = attributes[Attributes.IsAbstract];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "ReferenceType does not support the IsAbstract attribute.");
}
referenceTypeNode.IsAbstract = (bool)value.GetValue(typeof(bool));
// Symmetric Attribute
value = attributes[Attributes.Symmetric];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "ReferenceType does not support the Symmetric attribute.");
}
referenceTypeNode.Symmetric = (bool)value.GetValue(typeof(bool));
// InverseName Attribute
value = attributes[Attributes.InverseName];
if (value != null && value.Value != null)
{
referenceTypeNode.InverseName = (LocalizedText)value.GetValue(typeof(LocalizedText));
}
node = referenceTypeNode;
break;
}
case NodeClass.View:
{
ViewNode viewNode = new ViewNode();
// EventNotifier Attribute
value = attributes[Attributes.EventNotifier];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "View does not support the EventNotifier attribute.");
}
viewNode.EventNotifier = (byte)value.GetValue(typeof(byte));
// ContainsNoLoops Attribute
value = attributes[Attributes.ContainsNoLoops];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "View does not support the ContainsNoLoops attribute.");
}
viewNode.ContainsNoLoops = (bool)value.GetValue(typeof(bool));
node = viewNode;
break;
}
}
// NodeId Attribute
value = attributes[Attributes.NodeId];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not support the NodeId attribute.");
}
node.NodeId = (NodeId)value.GetValue(typeof(NodeId));
node.NodeClass = (NodeClass)nodeClass.Value;
// BrowseName Attribute
value = attributes[Attributes.BrowseName];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not support the BrowseName attribute.");
}
node.BrowseName = (QualifiedName)value.GetValue(typeof(QualifiedName));
// DisplayName Attribute
value = attributes[Attributes.DisplayName];
if (value == null)
{
throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not support the DisplayName attribute.");
}
node.DisplayName = (LocalizedText)value.GetValue(typeof(LocalizedText));
// all optional attributes follow
// Description Attribute
if (attributes.TryGetValue(Attributes.Description, out value) &&
value != null && value.Value != null)
{
node.Description = (LocalizedText)value.GetValue(typeof(LocalizedText));
}
// WriteMask Attribute
if (attributes.TryGetValue(Attributes.WriteMask, out value) &&
value != null)
{
node.WriteMask = (uint)value.GetValue(typeof(uint));
}
// UserWriteMask Attribute
if (attributes.TryGetValue(Attributes.UserWriteMask, out value) &&
value != null)
{
node.UserWriteMask = (uint)value.GetValue(typeof(uint));
}
// RolePermissions Attribute
if (attributes.TryGetValue(Attributes.RolePermissions, out value) &&
value != null)
{
ExtensionObject[] rolePermissions = value.Value as ExtensionObject[];
if (rolePermissions != null)
{
node.RolePermissions = new RolePermissionTypeCollection();
foreach (ExtensionObject rolePermission in rolePermissions)
{
node.RolePermissions.Add(rolePermission.Body as RolePermissionType);
}
}
}
// UserRolePermissions Attribute
if (attributes.TryGetValue(Attributes.UserRolePermissions, out value) &&
value != null)
{
ExtensionObject[] userRolePermissions = value.Value as ExtensionObject[];
if (userRolePermissions != null)
{
node.UserRolePermissions = new RolePermissionTypeCollection();
foreach (ExtensionObject rolePermission in userRolePermissions)
{
node.UserRolePermissions.Add(rolePermission.Body as RolePermissionType);
}
}
}
// AccessRestrictions Attribute
if (attributes.TryGetValue(Attributes.AccessRestrictions, out value) &&
value != null)
{
node.AccessRestrictions = (ushort)value.GetValue(typeof(ushort));
}
return node;
}
/// <summary>
/// Create a dictionary of attributes to read for a nodeclass.
/// </summary>
private IDictionary<uint, DataValue> CreateAttributes(NodeClass nodeclass = NodeClass.Unspecified, bool optionalAttributes = true)
{
// Attributes to read for all types of nodes
var attributes = new SortedDictionary<uint, DataValue>() {
{ Attributes.NodeId, null },
{ Attributes.NodeClass, null },
{ Attributes.BrowseName, null },
{ Attributes.DisplayName, null },
};
switch (nodeclass)
{
case NodeClass.Object:
attributes.Add(Attributes.EventNotifier, null);
break;
case NodeClass.Variable:
attributes.Add(Attributes.DataType, null);
attributes.Add(Attributes.ValueRank, null);
attributes.Add(Attributes.ArrayDimensions, null);
attributes.Add(Attributes.AccessLevel, null);
attributes.Add(Attributes.UserAccessLevel, null);
attributes.Add(Attributes.Historizing, null);
attributes.Add(Attributes.MinimumSamplingInterval, null);
attributes.Add(Attributes.AccessLevelEx, null);
break;
case NodeClass.Method:
attributes.Add(Attributes.Executable, null);
attributes.Add(Attributes.UserExecutable, null);
break;
case NodeClass.ObjectType:
attributes.Add(Attributes.IsAbstract, null);
break;
case NodeClass.VariableType:
attributes.Add(Attributes.IsAbstract, null);
attributes.Add(Attributes.DataType, null);
attributes.Add(Attributes.ValueRank, null);
attributes.Add(Attributes.ArrayDimensions, null);
break;
case NodeClass.ReferenceType:
attributes.Add(Attributes.IsAbstract, null);
attributes.Add(Attributes.Symmetric, null);
attributes.Add(Attributes.InverseName, null);
break;
case NodeClass.DataType:
attributes.Add(Attributes.IsAbstract, null);
attributes.Add(Attributes.DataTypeDefinition, null);
break;
case NodeClass.View:
attributes.Add(Attributes.EventNotifier, null);
attributes.Add(Attributes.ContainsNoLoops, null);
break;
default:
// build complete list of attributes.
attributes = new SortedDictionary<uint, DataValue> {
{ Attributes.NodeId, null },
{ Attributes.NodeClass, null },
{ Attributes.BrowseName, null },
{ Attributes.DisplayName, null },
//{ Attributes.Description, null },
//{ Attributes.WriteMask, null },
//{ Attributes.UserWriteMask, null },
{ Attributes.DataType, null },
{ Attributes.ValueRank, null },
{ Attributes.ArrayDimensions, null },
{ Attributes.AccessLevel, null },
{ Attributes.UserAccessLevel, null },
{ Attributes.MinimumSamplingInterval, null },
{ Attributes.Historizing, null },
{ Attributes.EventNotifier, null },
{ Attributes.Executable, null },
{ Attributes.UserExecutable, null },
{ Attributes.IsAbstract, null },
{ Attributes.InverseName, null },
{ Attributes.Symmetric, null },
{ Attributes.ContainsNoLoops, null },
{ Attributes.DataTypeDefinition, null },
//{ Attributes.RolePermissions, null },
//{ Attributes.UserRolePermissions, null },
//{ Attributes.AccessRestrictions, null },
{ Attributes.AccessLevelEx, null }
};
break;
}
if (optionalAttributes)
{
attributes.Add(Attributes.Description, null);
attributes.Add(Attributes.WriteMask, null);
attributes.Add(Attributes.UserWriteMask, null);
attributes.Add(Attributes.RolePermissions, null);
attributes.Add(Attributes.UserRolePermissions, null);
attributes.Add(Attributes.AccessRestrictions, null);
}
return attributes;
}
#endregion
#region
/// <summary>
/// 读取一个节点的所有属性
/// </summary>
public async Task<List<OPCNodeAttribute>> ReadNoteAttributeAsync(string tag, uint attributesId, CancellationToken cancellationToken = default)
{
BrowseDescriptionCollection nodesToBrowse = new();
ReadValueIdCollection nodesToRead = new();
NodeId sourceId = new(tag);
ReadValueId nodeToRead = new()
{
NodeId = sourceId,
AttributeId = attributesId
};
nodesToRead.Add(nodeToRead);
BrowseDescription nodeToBrowse = new()
{
NodeId = sourceId,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HasProperty,
IncludeSubtypes = true,
NodeClassMask = 0,
ResultMask = (uint)BrowseResultMask.All
};
nodesToBrowse.Add(nodeToBrowse);
var result1 = await ReadNoteAttributeAsync(nodesToBrowse, nodesToRead, cancellationToken);
var result2 = result1.Values.FirstOrDefault();
return result2;
}
/// <summary>
/// 读取节点的所有属性
/// </summary>
public async Task<Dictionary<string, List<OPCNodeAttribute>>> ReadNoteAttributeAsync(List<string> tags, CancellationToken cancellationToken)
{
BrowseDescriptionCollection nodesToBrowse = new();
ReadValueIdCollection nodesToRead = new();
foreach (var tag in tags)
{
NodeId sourceId = new(tag);
for (uint ii = Attributes.NodeClass; ii <= Attributes.UserExecutable; ii++)
{
ReadValueId nodeToRead = new()
{
NodeId = sourceId,
AttributeId = ii
};
nodesToRead.Add(nodeToRead);
}
BrowseDescription nodeToBrowse = new()
{
NodeId = sourceId,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HasProperty,
IncludeSubtypes = true,
NodeClassMask = 0,
ResultMask = (uint)BrowseResultMask.All
};
nodesToBrowse.Add(nodeToBrowse);
}
return await ReadNoteAttributeAsync(nodesToBrowse, nodesToRead, cancellationToken);
}
/// <summary>
/// 读取一个节点的所有属性
/// </summary>
/// <param name="tag">节点信息</param>
/// <returns>节点的特性值</returns>
public OPCNodeAttribute[] ReadNoteAttributes(string tag)
{
NodeId sourceId = new(tag);
ReadValueIdCollection nodesToRead = new();
for (uint ii = Attributes.NodeClass; ii <= Attributes.UserExecutable; ii++)
{
ReadValueId nodeToRead = new()
{
NodeId = sourceId,
AttributeId = ii
};
nodesToRead.Add(nodeToRead);
}
int startOfProperties = nodesToRead.Count;
// find all of the pror of the node.
BrowseDescription nodeToBrowse1 = new()
{
NodeId = sourceId,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HasProperty,
IncludeSubtypes = true,
NodeClassMask = 0,
ResultMask = (uint)BrowseResultMask.All
};
BrowseDescriptionCollection nodesToBrowse = new()
{
nodeToBrowse1
};
// fetch property references from the server.
ReferenceDescriptionCollection references = FormUtils.Browse(m_session, nodesToBrowse, false);
if (references == null)
{
return Array.Empty<OPCNodeAttribute>();
}
for (int ii = 0; ii < references.Count; ii++)
{
// ignore external references.
if (references[ii].NodeId.IsAbsolute)
{
continue;
}
ReadValueId nodeToRead = new()
{
NodeId = (NodeId)references[ii].NodeId,
AttributeId = Attributes.Value
};
nodesToRead.Add(nodeToRead);
}
// read all values.
m_session.Read(
null,
0,
TimestampsToReturn.Neither,
nodesToRead,
out DataValueCollection results,
out DiagnosticInfoCollection diagnosticInfos);
ClientBase.ValidateResponse(results, nodesToRead);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);
// process results.
List<OPCNodeAttribute> nodeAttribute = new();
for (int ii = 0; ii < results.Count; ii++)
{
OPCNodeAttribute item = new();
// process attribute value.
if (ii < startOfProperties)
{
// ignore attributes which are invalid for the node.
if (results[ii].StatusCode == StatusCodes.BadAttributeIdInvalid)
{
continue;
}
// get the name of the attribute.
item.Name = Attributes.GetBrowseName(nodesToRead[ii].AttributeId);
// display any unexpected error.
if (StatusCode.IsBad(results[ii].StatusCode))
{
item.Type = Utils.Format("{0}", Attributes.GetDataTypeId(nodesToRead[ii].AttributeId));
item.Value = Utils.Format("{0}", results[ii].StatusCode);
}
// display the value.
else
{
TypeInfo typeInfo = TypeInfo.Construct(results[ii].Value);
item.Type = typeInfo.BuiltInType.ToString();
if (typeInfo.ValueRank >= ValueRanks.OneOrMoreDimensions)
{
item.Type += "[]";
}
item.Value = results[ii].Value;//Utils.Format("{0}", results[ii].Value);
}
}
// process property value.
else
{
// ignore properties which are invalid for the node.
if (results[ii].StatusCode == StatusCodes.BadNodeIdUnknown)
{
continue;
}
// get the name of the property.
item.Name = Utils.Format("{0}", references[ii - startOfProperties]);
// display any unexpected error.
if (StatusCode.IsBad(results[ii].StatusCode))
{
item.Type = String.Empty;
item.Value = Utils.Format("{0}", results[ii].StatusCode);
}
// display the value.
else
{
TypeInfo typeInfo = TypeInfo.Construct(results[ii].Value);
item.Type = typeInfo.BuiltInType.ToString();
if (typeInfo.ValueRank >= ValueRanks.OneOrMoreDimensions)
{
item.Type += "[]";
}
item.Value = results[ii].Value;
}
}
nodeAttribute.Add(item);
}
return nodeAttribute.ToArray();
}
#endregion
/// <inheritdoc/>
public void Dispose()
{
Disconnect();
}
#region
private void CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs eventArgs)
{
if (ServiceResult.IsGood(eventArgs.Error))
eventArgs.Accept = true;
else if (eventArgs.Error.StatusCode.Code == StatusCodes.BadCertificateUntrusted)
eventArgs.Accept = true;
else
throw new Exception(string.Format("验证证书失败,错误代码:{0}: {1}", eventArgs.Error.Code, eventArgs.Error.AdditionalInfo));
}
private async Task<Dictionary<string, List<OPCNodeAttribute>>> ReadNoteAttributeAsync(BrowseDescriptionCollection nodesToBrowse, ReadValueIdCollection nodesToRead, CancellationToken cancellationToken)
{
int startOfProperties = nodesToRead.Count;
ReferenceDescriptionCollection references = await FormUtils.BrowseAsync(m_session, nodesToBrowse, false, cancellationToken);
if (references == null)
{
throw new("浏览失败");
}
for (int ii = 0; ii < references.Count; ii++)
{
if (references[ii].NodeId.IsAbsolute)
{
continue;
}
ReadValueId nodeToRead = new()
{
NodeId = (NodeId)references[ii].NodeId,
AttributeId = Attributes.Value
};
nodesToRead.Add(nodeToRead);
}
var result = await m_session.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
nodesToRead, cancellationToken);
ClientBase.ValidateResponse(result.Results, nodesToRead);
ClientBase.ValidateDiagnosticInfos(result.DiagnosticInfos, nodesToRead);
Dictionary<string, List<OPCNodeAttribute>> nodeAttributes = new();
for (int ii = 0; ii < result.Results.Count; ii++)
{
DataValue nodeValue = result.Results[ii];
var nodeToRead = nodesToRead[ii];
OPCNodeAttribute item = new();
if (ii < startOfProperties)
{
if (nodeValue.StatusCode == StatusCodes.BadAttributeIdInvalid)
{
continue;
}
item.Name = Attributes.GetBrowseName(nodesToRead[ii].AttributeId);
if (StatusCode.IsBad(nodeValue.StatusCode))
{
item.Type = Utils.Format("{0}", Attributes.GetDataTypeId(nodesToRead[ii].AttributeId));
item.Value = Utils.Format("{0}", nodeValue.StatusCode);
}
else
{
TypeInfo typeInfo = TypeInfo.Construct(nodeValue.Value);
item.Type = typeInfo.BuiltInType.ToString();
if (typeInfo.ValueRank >= ValueRanks.OneOrMoreDimensions)
{
item.Type += "[]";
}
if (item.Name == nameof(Attributes.NodeClass))
{
item.Value = ((NodeClass)nodeValue.Value).ToString();
}
else if (item.Name == nameof(Attributes.EventNotifier))
{
item.Value = ((EventNotifierType)nodeValue.Value).ToString();
}
else
item.Value = nodeValue.Value;
}
}
if (nodeAttributes.ContainsKey(nodeToRead.NodeId.ToString()))
{
nodeAttributes[nodeToRead.NodeId.ToString()].Add(item);
}
else
{
nodeAttributes.Add(nodeToRead.NodeId.ToString(), new() { item });
}
}
return nodeAttributes;
}
/// <summary>
/// 连接处理器连接事件处理完成。
/// </summary>
private void Server_ReconnectComplete(object sender, EventArgs e)
{
if (!Object.ReferenceEquals(sender, m_reConnectHandler))
{
return;
}
m_session = m_reConnectHandler.Session;
m_reConnectHandler = null;
}
private void Session_KeepAlive(ISession session, KeepAliveEventArgs e)
{
lock (checkLock)
{
if (!Object.ReferenceEquals(session, m_session))
{
return;
}
if (ServiceResult.IsBad(e.Status))
{
_logAction?.Invoke(3, this, $"心跳检测错误:{e.Status}", null);
if (m_reConnectHandler == null)
{
m_reConnectHandler = new SessionReconnectHandler();
m_reConnectHandler.BeginReconnect(m_session, m_session.KeepAliveInterval, Server_ReconnectComplete);
}
return;
}
}
}
#endregion
}