Files
ThingsGateway/src/Plugin/ThingsGateway.Foundation.OpcUa/OpcUaMaster/OpcUaMaster.cs
2025-10-18 23:14:55 +08:00

1429 lines
51 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.

//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
//修改自https://github.com/dathlin/OpcUaHelper 与OPC基金会net库
using System.Collections.Concurrent;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
namespace ThingsGateway.Foundation.OpcUa;
/// <summary>
/// 订阅委托
/// </summary>
/// <param name="value"></param>
public delegate void DataChangedEventHandler((VariableNode variableNode, MonitoredItem monitoredItem, DataValue dataValue, JToken jToken) value);
/// <summary>
/// 日志输出
/// </summary>
public delegate void LogEventHandler(byte level, object sender, string message, Exception ex);
/// <summary>
/// OpcUaMaster
/// </summary>
public class OpcUaMaster : IAsyncDisposable
{
#region
/// <summary>
/// Raised after the client status change
/// </summary>
public LogEventHandler LogEvent;
/// <summary>
/// 当前配置
/// </summary>
public OpcUaProperty OpcUaProperty;
/// <summary>
/// ProductUri
/// </summary>
public string ProductUri = "https://thingsgateway.cn/";
/// <summary>
/// 当前的变量名称/OPC变量节点
/// </summary>
private readonly ConcurrentDictionary<string, VariableNode> _variableDicts = new();
private readonly object checkLock = new();
/// <summary>
/// 当前的订阅组,组名称/组
/// </summary>
private readonly ConcurrentDictionary<string, Subscription> _subscriptionDicts = new();
private readonly ApplicationInstance m_application = new();
private readonly ApplicationConfiguration m_configuration;
private EventHandler<bool> m_ConnectComplete;
private EventHandler<KeepAliveEventArgs> m_KeepAliveComplete;
private EventHandler m_ReconnectComplete;
private SessionReconnectHandler m_reConnectHandler;
private EventHandler m_ReconnectStarting;
private ISession m_session;
private ComplexTypeSystem typeSystem;
/// <summary>
/// 默认的构造函数实例化一个新的OPC UA类
/// </summary>
public OpcUaMaster()
{
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 = true,
AddAppCertToTrustedStore = true,
SendCertificateChain = true,
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 = 300000,
SecurityTokenLifetime = 3600000
},
ClientConfiguration = new ClientConfiguration
{
DefaultSessionTimeout = 60000,
MinSubscriptionLifetime = 10000,
},
};
Task.Run(() => certificateValidator.UpdateAsync(m_configuration)).GetAwaiter().GetResult();
Task.Run(() => m_configuration.ValidateAsync(ApplicationType.Client)).GetAwaiter().GetResult();
m_application.ApplicationConfiguration = m_configuration;
}
/// <summary>
/// Raised after successfully connecting to or disconnecing from a server.
/// </summary>
public event EventHandler<bool> ConnectComplete
{
add { m_ConnectComplete += value; }
remove { m_ConnectComplete -= value; }
}
/// <summary>
/// 订阅
/// </summary>
public event DataChangedEventHandler DataChangedHandler;
/// <summary>
/// Raised when a good keep alive from the server arrives.
/// </summary>
public event EventHandler<KeepAliveEventArgs> KeepAliveComplete
{
add { m_KeepAliveComplete += value; }
remove { m_KeepAliveComplete -= value; }
}
/// <summary>
/// Raised when a reconnect operation completes.
/// </summary>
public event EventHandler ReconnectComplete
{
add { m_ReconnectComplete += value; }
remove { m_ReconnectComplete -= value; }
}
/// <summary>
/// Raised when a reconnect operation starts.
/// </summary>
public event EventHandler ReconnectStarting
{
add { m_ReconnectStarting += value; }
remove { m_ReconnectStarting -= value; }
}
/// <summary>
/// 配置信息
/// </summary>
public ApplicationConfiguration AppConfig => m_configuration;
/// <summary>
/// 连接状态
/// </summary>
public bool Connected => m_session?.Connected == true && connected;
private bool connected = false;
/// <summary>
/// OpcUaMaster
/// </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, bool loadType = true, CancellationToken cancellationToken = default)
{
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();
var variableNodes = loadType ? await ReadNodesAsync(items, false, cancellationToken).ConfigureAwait(false) : null;
for (int i = 0; i < items.Length; i++)
{
try
{
var item = new MonitoredItem
{
StartNodeId = items[i],
AttributeId = Attributes.Value,
DisplayName = items[i],
Filter = OpcUaProperty.DeadBand == 0 ? null : new DataChangeFilter() { DeadbandValue = OpcUaProperty.DeadBand, DeadbandType = (int)DeadbandType.Absolute, Trigger = DataChangeTrigger.StatusValue },
SamplingInterval = OpcUaProperty?.UpdateRate ?? 1000,
};
item.Notification += Callback;
monitoredItems.Add(item);
}
catch (Exception ex)
{
Log(3, ex, $"Failed to initialize {items[i]} variable subscription");
}
}
m_session.AddSubscription(m_subscription);
try
{
await m_subscription.CreateAsync(cancellationToken).ConfigureAwait(false);
await Task.Delay(100, cancellationToken).ConfigureAwait(false); // allow for subscription to be finished on server?
}
catch (Exception)
{
try
{
await m_subscription.CreateAsync(cancellationToken).ConfigureAwait(false);
await Task.Delay(100, cancellationToken).ConfigureAwait(false); // allow for subscription to be finished on server?
}
catch (Exception)
{
await m_session.RemoveSubscriptionAsync(m_subscription, cancellationToken).ConfigureAwait(false);
throw;
}
}
m_subscription.AddItems(monitoredItems);
foreach (var item in m_subscription.MonitoredItems.Where(a => a.Status.Error != null && StatusCode.IsBad(a.Status.Error.StatusCode)))
{
item.Filter = OpcUaProperty.DeadBand == 0 ? null : new DataChangeFilter() { DeadbandValue = OpcUaProperty.DeadBand, DeadbandType = (int)DeadbandType.None, Trigger = DataChangeTrigger.StatusValue };
}
await m_subscription.ApplyChangesAsync(cancellationToken).ConfigureAwait(false);
var isError = m_subscription.MonitoredItems.Any(a => a.Status.Error != null && StatusCode.IsBad(a.Status.Error.StatusCode));
if (isError)
{
Log(3, null, $"Failed to create subscription for the following variables{Environment.NewLine}{m_subscription.MonitoredItems.Where(
a => a.Status.Error != null && StatusCode.IsBad(a.Status.Error.StatusCode))
.Select(a => $"{a.StartNodeId}{a.Status.Error}").ToJsonString()}");
}
if (_subscriptionDicts.TryAdd(subscriptionName, m_subscription))
{
}
else if (_subscriptionDicts.TryGetValue(subscriptionName, out var existingSubscription))
{
// remove
await existingSubscription.DeleteAsync(true, cancellationToken).ConfigureAwait(false);
await m_session.RemoveSubscriptionAsync(existingSubscription, cancellationToken).ConfigureAwait(false);
try { existingSubscription.Dispose(); } catch { }
_subscriptionDicts[subscriptionName] = m_subscription;
}
}
/// <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 OpcUaUtils.BrowseAsync(m_session, nodesToBrowse, false).ConfigureAwait(false);
return references.ToArray();
}
/// <summary>
/// 调用服务器的方法
/// </summary>
/// <param name="tagParent">方法的父节点tag</param>
/// <param name="tag">方法的节点tag</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <param name="args">传递的参数</param>
/// <returns>输出的结果值</returns>
public async Task<IList<object>> CallMethodByNodeIdAsync(string tagParent, string tag, CancellationToken cancellationToken, params object[] args)
{
if (m_session == null)
{
return null;
}
IList<object> outputArguments = await m_session.CallAsync(
new NodeId(tagParent),
new NodeId(tag),
cancellationToken,
args).ConfigureAwait(false);
return outputArguments.ToArray();
}
/// <summary>
/// 连接到服务器
/// </summary>
public Task ConnectAsync(CancellationToken cancellationToken)
{
return ConnectAsync(OpcUaProperty.OpcUrl, cancellationToken);
}
/// <summary>
/// 断开连接。
/// </summary>
public Task DisconnectAsync()
{
return PrivateDisconnectAsync();
}
public async ValueTask DisposeAsync()
{
await DisconnectAsync().ConfigureAwait(false);
_variableDicts?.Clear();
_subscriptionDicts?.Clear();
waitLock?.Dispose();
}
/// <summary>
/// 获取变量说明
/// </summary>
/// <returns></returns>
public string GetAddressDescription()
{
return "ItemId";
}
/// <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).ConfigureAwait(false);
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;
}
/// <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).ConfigureAwait(false);
return result;
}
/// <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).ConfigureAwait(false);
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).ConfigureAwait(false);
}
/// <summary>
/// 读取一个节点的所有属性
/// </summary>
/// <param name="tag">节点信息</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns>节点的特性值</returns>
public async Task<OPCNodeAttribute[]> ReadNoteAttributesAsync(string tag, CancellationToken cancellationToken)
{
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 = await OpcUaUtils.BrowseAsync(m_session, nodesToBrowse, false, cancellationToken).ConfigureAwait(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);
}
// 读取当前的值
var result = await m_session.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
nodesToRead,
cancellationToken).ConfigureAwait(false);
var results = result.Results;
var diagnosticInfos = result.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 = 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();
}
/// <summary>
/// Returns the browse name for the attribute.
/// </summary>
public static string GetBrowseName(uint identifier)
{
return s_attributesIdToName.Value.TryGetValue(identifier, out string name)
? name : string.Empty;
}
/// <summary>
/// Creates a dictionary of identifiers to browse names for the attributes.
/// </summary>
private static readonly Lazy<IReadOnlyDictionary<long, string>> s_attributesIdToName =
new(() =>
{
System.Reflection.FieldInfo[] fields = typeof(Attributes).GetFields(
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var keyValuePairs = new Dictionary<long, string>();
foreach (System.Reflection.FieldInfo field in fields)
{
keyValuePairs.TryAdd(Convert.ToInt64(field.GetValue(typeof(Attributes))), field.Name);
}
#if NET8_0_OR_GREATER
return keyValuePairs.ToFrozenDictionary();
#else
return new System.Collections.ObjectModel.ReadOnlyDictionary<long, string>(keyValuePairs);
#endif
});
/// <summary>
/// 移除所有的订阅消息
/// </summary>
public async Task RemoveAllSubscriptionAsync()
{
foreach (var item in _subscriptionDicts)
{
await item.Value.DeleteAsync(true).ConfigureAwait(false);
await m_session.RemoveSubscriptionAsync(item.Value).ConfigureAwait(false);
try { item.Value.Dispose(); } catch { }
}
_subscriptionDicts.Clear();
}
/// <summary>
/// 移除订阅消息
/// </summary>
/// <param name="subscriptionName">组名称</param>
public async Task RemoveSubscriptionAsync(string subscriptionName)
{
if (_subscriptionDicts.TryGetValue(subscriptionName, out var subscription))
{
// remove
await subscription.DeleteAsync(true).ConfigureAwait(false);
await m_session.RemoveSubscriptionAsync(subscription).ConfigureAwait(false);
try { subscription.Dispose(); } catch { }
_subscriptionDicts.TryRemove(subscriptionName, out _);
}
}
/// <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, false, cancellationToken).ConfigureAwait(false);
var dataValue = JsonUtils.Decode(
m_session.MessageContext,
variableNode.DataType,
await TypeInfo.GetBuiltInTypeAsync(variableNode.DataType, m_session.SystemContext.TypeTable, cancellationToken).ConfigureAwait(false),
item.Value.CalculateActualValueRank(),
item.Value
);
valueToWrite.Value = dataValue;
valuesToWrite.Add(valueToWrite);
}
var result = await m_session.WriteAsync(
requestHeader: null,
nodesToWrite: valuesToWrite, cancellationToken).ConfigureAwait(false);
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(false, result.Results[i].ToString()));
else
{
results.Add(keys[i], Tuple.Create(true, "Success"));
}
}
return results;
}
catch (Exception ex)
{
var keys = writeInfoLists.Keys.ToList();
foreach (var item in keys)
{
results.Add(item, Tuple.Create(false, ex.Message));
}
return results;
}
}
private void Callback(MonitoredItem monitoreditem, MonitoredItemNotificationEventArgs monitoredItemNotificationEventArgs)
{
try
{
if (m_session != null)
{
foreach (var value in monitoreditem.DequeueValues())
{
var variableNode = ReadNode(monitoreditem.StartNodeId.ToString(), false, StatusCode.IsGood(value.StatusCode)).GetAwaiter().GetResult();
if (value.Value != null)
{
if (value.Value.GetType().IsRichPrimitive())
{
DataChangedHandler?.Invoke((variableNode, monitoreditem, value, null));
continue;
}
var data = JsonUtils.Encode(m_session.MessageContext, TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable), value.Value);
if (data == null && value.Value != null)
{
Log(3, null, $"{monitoreditem.StartNodeId}Conversion error, original value is{value.Value}");
var data1 = JsonUtils.Encode(m_session.MessageContext, TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable), value.Value);
}
DataChangedHandler?.Invoke((variableNode, monitoreditem, value, data!));
}
else
{
var data = JValue.CreateNull();
DataChangedHandler?.Invoke((variableNode, monitoreditem, value, data));
}
}
}
}
catch (Exception ex)
{
Log(3, ex, $"{monitoreditem.StartNodeId} - Subscription processing error");
}
}
#endregion
#region
/// <summary>
/// 检查/创建证书
/// </summary>
/// <returns></returns>
public async Task CheckApplicationInstanceCertificate()
{
await m_application.CheckApplicationInstanceCertificatesAsync(true, 1200).ConfigureAwait(false);
}
SemaphoreSlim waitLock = new(1, 1);
private string LastServerUrl { get; set; }
/// <summary>
/// Creates a new session.
/// </summary>
/// <returns>The new session object.</returns>
private async Task ConnectAsync(string serverUrl, CancellationToken cancellationToken)
{
if (m_session != null)
{
return;
}
try
{
await waitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
if (m_session != null)
{
return;
}
await PrivateDisconnectAsync().ConfigureAwait(false);
if (LastServerUrl != serverUrl)
{
_variableDicts.Clear();
}
LastServerUrl = serverUrl;
if (m_configuration == null)
{
throw new ArgumentNullException(nameof(m_configuration));
}
var useSecurity = OpcUaProperty?.UseSecurity ?? true;
EndpointDescription endpointDescription = await OpcUaUtils.SelectEndpointAsync(m_configuration, serverUrl, useSecurity, 10000).ConfigureAwait(false);
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration);
ConfiguredEndpoint endpoint = new(null, endpointDescription, endpointConfiguration);
UserIdentity userIdentity;
if (!string.IsNullOrEmpty(OpcUaProperty.UserName))
{
userIdentity = new UserIdentity(OpcUaProperty.UserName, OpcUaProperty.Password);
}
else
{
userIdentity = new UserIdentity(new AnonymousIdentityToken());
}
//创建本地证书
if (useSecurity)
await m_application.CheckApplicationInstanceCertificatesAsync(true, 1200, cancellationToken).ConfigureAwait(false);
m_session = await Opc.Ua.Client.Session.CreateAsync(
DefaultSessionFactory.Instance,
m_configuration,
(ITransportWaitingConnection)null,
endpoint,
false,
OpcUaProperty.CheckDomain,
(string.IsNullOrEmpty(OPCUAName)) ? m_configuration.ApplicationName : OPCUAName,
600000,
userIdentity,
null, cancellationToken
).ConfigureAwait(false);
connected = true;
m_session.KeepAliveInterval = OpcUaProperty.KeepAliveInterval == 0 ? 60000 : OpcUaProperty.KeepAliveInterval;
m_session.KeepAlive += Session_KeepAlive;
typeSystem = new ComplexTypeSystem(m_session);
// raise an event.
DoConnectComplete(true);
Log(2, null, "Connected");
}
finally
{
waitLock.Release();
}
}
private async Task PrivateDisconnectAsync()
{
bool state = m_session?.Connected == true;
connected = false;
if (m_reConnectHandler != null)
{
try { m_reConnectHandler.Dispose(); } catch { }
m_reConnectHandler = null;
}
if (m_session != null)
{
m_session.KeepAlive -= Session_KeepAlive;
await m_session.CloseAsync(10000).ConfigureAwait(false);
m_session.Dispose();
m_session = null;
}
if (state)
{
Log(2, null, "Disconnected");
DoConnectComplete(false);
}
}
#endregion
#region /
/// <summary>
/// 从服务器读取值
/// </summary>
private async Task<List<(string, DataValue, JToken)>> ReadJTokenValueAsync(NodeId[] nodeIds, CancellationToken cancellationToken = default)
{
if (m_session == null)
{
throw new ArgumentNullException(nameof(m_session));
}
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.Both,
nodesToRead,
cancellationToken).ConfigureAwait(false);
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, StatusCode.IsGood(results[i].StatusCode), cancellationToken).ConfigureAwait(false);
var type = await TypeInfo.GetBuiltInTypeAsync(variableNode.DataType, m_session.SystemContext.TypeTable, cancellationToken).ConfigureAwait(false);
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> ReadNode(string nodeIdStr, bool isOnlyServer = true, bool cache = true, CancellationToken cancellationToken = default)
{
if (!isOnlyServer)
{
if (_variableDicts.TryGetValue(nodeIdStr, out var value))
{
return value;
}
}
NodeId nodeToRead = new(nodeIdStr);
uint[] attributes = new uint[] { Attributes.NodeId, Attributes.DataType };
// build list of values to read.
ReadValueIdCollection itemsToRead = new ReadValueIdCollection();
foreach (uint attributeId in attributes)
{
ReadValueId itemToRead = new ReadValueId
{
NodeId = nodeToRead,
AttributeId = attributeId
};
itemsToRead.Add(itemToRead);
}
// read from server.
var result = await m_session.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
itemsToRead,
cancellationToken).ConfigureAwait(false);
var values = result.Results;
var diagnosticInfos = result.DiagnosticInfos;
var responseHeader = result.ResponseHeader;
VariableNode variableNode = GetVariableNodes(itemsToRead, values, diagnosticInfos, responseHeader).FirstOrDefault();
if (cache)
_variableDicts.AddOrUpdate(nodeIdStr, a => variableNode, (a, b) => variableNode);
return variableNode;
}
/// <summary>
/// 从服务器或缓存读取节点
/// </summary>
private async Task<VariableNode> ReadNodeAsync(string nodeIdStr, bool isOnlyServer = true, bool cache = true, CancellationToken cancellationToken = default)
{
if (!isOnlyServer)
{
if (_variableDicts.TryGetValue(nodeIdStr, out var value))
{
return value;
}
}
NodeId nodeToRead = new(nodeIdStr);
uint[] attributes = new uint[] { Attributes.NodeId, Attributes.DataType };
// build list of values to read.
ReadValueIdCollection itemsToRead = new ReadValueIdCollection();
foreach (uint attributeId in attributes)
{
ReadValueId itemToRead = new ReadValueId
{
NodeId = nodeToRead,
AttributeId = attributeId
};
itemsToRead.Add(itemToRead);
}
// read from server.
ReadResponse readResponse = await m_session.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
itemsToRead, cancellationToken).ConfigureAwait(false);
DataValueCollection values = readResponse.Results;
DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos;
var responseHeader = readResponse.ResponseHeader;
VariableNode variableNode = GetVariableNodes(itemsToRead, values, diagnosticInfos, responseHeader).FirstOrDefault();
if (OpcUaProperty.LoadType && variableNode.DataType != NodeId.Null && (await TypeInfo.GetBuiltInTypeAsync(variableNode.DataType, m_session.SystemContext.TypeTable, cancellationToken).ConfigureAwait(false)) == BuiltInType.ExtensionObject)
await typeSystem.LoadTypeAsync(variableNode.DataType, ct: cancellationToken).ConfigureAwait(false);
if (cache)
_variableDicts.AddOrUpdate(nodeIdStr, a => variableNode, (a, b) => variableNode);
return variableNode;
}
private List<VariableNode> GetVariableNodes(ReadValueIdCollection itemsToRead, DataValueCollection values, DiagnosticInfoCollection diagnosticInfos, ResponseHeader responseHeader, int count = 1)
{
ClientBase.ValidateResponse(values, itemsToRead);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead);
List<VariableNode> variableNodes = new();
for (int i = 0; i < count; i++)
{
VariableNode variableNode = new VariableNode();
DataValue value = values[1 + 2 * i];
//if (!DataValue.IsGood(value))
//{
// //throw ServiceResultException.Create(values[1+2 * i].StatusCode, 1+2 * i, diagnosticInfos, responseHeader.StringTable);
//}
// DataType Attribute
if (value == null)
{
//throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the DataType attribute.");
variableNode.DataType = NodeId.Null;
}
else
{
variableNode.DataType = (NodeId)value.GetValue(typeof(NodeId));
}
value = values[0 + 2 * i];
// NodeId Attribute
if (!DataValue.IsGood(value))
{
Log(3, ServiceResultException.Create(value.StatusCode, 0 + 2 * i, diagnosticInfos, responseHeader.StringTable), $"Get nodeid {itemsToRead[0 + 2 * i].NodeId} fail");
}
if (value == null)
{
Log(3, ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not support the NodeId attribute."), $"Get nodeid {itemsToRead[0 + 2 * i].NodeId} fail");
}
variableNode.NodeId = (NodeId)value.GetValue(typeof(NodeId));
variableNodes.Add(variableNode);
}
return variableNodes;
}
/// <summary>
/// 从服务器读取节点
/// </summary>
private async Task<List<Node>> ReadNodesAsync(string[] nodeIdStrs, bool cache = false, CancellationToken cancellationToken = default)
{
List<Node> result = new(nodeIdStrs.Length);
foreach (var items in nodeIdStrs.ChunkBetter(OpcUaProperty.GroupSize))
{
uint[] attributes = new uint[] { Attributes.NodeId, Attributes.DataType };
// build list of values to read.
ReadValueIdCollection itemsToRead = new ReadValueIdCollection();
foreach (var item in items)
{
foreach (uint attributeId in attributes)
{
ReadValueId itemToRead = new ReadValueId
{
NodeId = item,
AttributeId = attributeId
};
itemsToRead.Add(itemToRead);
}
}
// read from server.
ReadResponse readResponse = await m_session.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
itemsToRead, cancellationToken).ConfigureAwait(false);
DataValueCollection values = readResponse.Results;
DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos;
var responseHeader = readResponse.ResponseHeader;
var variableNodes = GetVariableNodes(itemsToRead, values, diagnosticInfos, responseHeader, nodeIdStrs.Length);
for (int i = 0; i < variableNodes.Count; i++)
{
var node = variableNodes[i];
if (_variableDicts.TryGetValue(nodeIdStrs[i], out var value))
{
}
else
{
if (cache)
_variableDicts.AddOrUpdate(nodeIdStrs[i], a => node, (a, b) => node);
if (node.DataType != NodeId.Null && (await TypeInfo.GetBuiltInTypeAsync(node.DataType, m_session.SystemContext.TypeTable, cancellationToken).ConfigureAwait(false)) == BuiltInType.ExtensionObject)
{
await typeSystem.LoadTypeAsync(node.DataType, ct: cancellationToken).ConfigureAwait(false);
}
}
result.Add(node);
}
}
return result;
}
#endregion /
#region
private void CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs eventArgs)
{
if (ServiceResult.IsGood(eventArgs.Error))
eventArgs.Accept = true;
else if (OpcUaProperty.AutoAcceptUntrustedCertificates)
eventArgs.Accept = true;
else
throw new Exception(string.Format("Verification certificate failed with error code: {0}: {1}", eventArgs.Error.Code, eventArgs.Error.AdditionalInfo));
}
/// <summary>
/// Raises the connect complete event on the main GUI thread.
/// </summary>
private void DoConnectComplete(bool state)
{
m_ConnectComplete?.Invoke(this, state);
}
/// <summary>
/// Report the client status
/// </summary>
/// <param name="logLevel">Whether the status represents an error. </param>
/// <param name="exception">exception</param>
/// <param name="status">The status message.</param>
/// <param name="args">Arguments used to format the status message.</param>
private void Log(byte logLevel, Exception exception, string status, params object[] args)
{
LogEvent?.Invoke(logLevel, this, string.Format(status, args), exception);
}
private async Task<Dictionary<string, List<OPCNodeAttribute>>> ReadNoteAttributeAsync(BrowseDescriptionCollection nodesToBrowse, ReadValueIdCollection nodesToRead, CancellationToken cancellationToken)
{
int startOfProperties = nodesToRead.Count;
ReferenceDescriptionCollection references = await OpcUaUtils.BrowseAsync(m_session, nodesToBrowse, false, cancellationToken).ConfigureAwait(false);
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).ConfigureAwait(false);
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 = 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)
{
try
{
Log(2, null, "Reconnected : success");
if (!Object.ReferenceEquals(sender, m_reConnectHandler))
{
return;
}
// if session recovered, Session property is null
if (m_reConnectHandler.Session != null)
{
// ensure only a new instance is disposed
// after reactivate, the same session instance may be returned
if (!Object.ReferenceEquals(m_session, m_reConnectHandler.Session))
{
var session = m_session;
m_session = m_reConnectHandler.Session;
connected = true;
Utils.SilentDispose(session);
}
}
m_reConnectHandler.Dispose();
m_reConnectHandler = null;
// raise any additional notifications.
m_ReconnectComplete?.Invoke(this, e);
}
catch (Exception ex)
{
Log(3, ex, $"{nameof(Server_ReconnectComplete)}");
}
}
private void Session_KeepAlive(ISession session, KeepAliveEventArgs e)
{
lock (checkLock)
{
if (!Object.ReferenceEquals(session, m_session))
{
return;
}
if (ServiceResult.IsBad(e.Status))
{
connected = false;
if (m_reConnectHandler == null)
{
Log(3, null, "Reconnecting : {0}", e.Status.ToString());
m_ReconnectStarting?.Invoke(this, e);
m_reConnectHandler = new SessionReconnectHandler(true, 10000);
m_reConnectHandler.BeginReconnect(m_session, 1000, Server_ReconnectComplete);
e.CancelKeepAlive = true;
}
}
// raise any additional notifications.
m_KeepAliveComplete?.Invoke(this, e);
}
}
#endregion
}