1129 lines
38 KiB
C#
1129 lines
38 KiB
C#
#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 Opc.Ua;
|
||
using Opc.Ua.Client;
|
||
|
||
using System.Text;
|
||
|
||
namespace ThingsGateway.Foundation.Adapter.OPCUA;
|
||
/// <summary>
|
||
/// 辅助类
|
||
/// </summary>
|
||
public class FormUtils
|
||
{
|
||
/// <summary>
|
||
/// Browses the address space and returns the references found.
|
||
/// </summary>
|
||
/// <param name="session">The session.</param>
|
||
/// <param name="nodesToBrowse">The set of browse operations to perform.</param>
|
||
/// <param name="throwOnError">if set to <c>true</c> a exception will be thrown on an error.</param>
|
||
/// <returns>
|
||
/// The references found. Null if an error occurred.
|
||
/// </returns>
|
||
public static ReferenceDescriptionCollection Browse(ISession session, BrowseDescriptionCollection nodesToBrowse, bool throwOnError)
|
||
{
|
||
try
|
||
{
|
||
ReferenceDescriptionCollection references = new();
|
||
BrowseDescriptionCollection unprocessedOperations = new();
|
||
|
||
while (nodesToBrowse.Count > 0)
|
||
{
|
||
// start the browse operation.
|
||
|
||
session.Browse(
|
||
null,
|
||
null,
|
||
0,
|
||
nodesToBrowse,
|
||
out BrowseResultCollection results,
|
||
out DiagnosticInfoCollection diagnosticInfos);
|
||
|
||
ClientBase.ValidateResponse(results, nodesToBrowse);
|
||
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse);
|
||
|
||
ByteStringCollection continuationPoints = new();
|
||
|
||
for (int ii = 0; ii < nodesToBrowse.Count; ii++)
|
||
{
|
||
// check for error.
|
||
if (StatusCode.IsBad(results[ii].StatusCode))
|
||
{
|
||
// this error indicates that the server does not have enough simultaneously active
|
||
// continuation points. This request will need to be resent after the other operations
|
||
// have been completed and their continuation points released.
|
||
if (results[ii].StatusCode == StatusCodes.BadNoContinuationPoints)
|
||
{
|
||
unprocessedOperations.Add(nodesToBrowse[ii]);
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
// check if all references have been fetched.
|
||
if (results[ii].References.Count == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// save results.
|
||
references.AddRange(results[ii].References);
|
||
|
||
// check for continuation point.
|
||
if (results[ii].ContinuationPoint != null)
|
||
{
|
||
continuationPoints.Add(results[ii].ContinuationPoint);
|
||
}
|
||
}
|
||
|
||
// process continuation points.
|
||
ByteStringCollection revisedContiuationPoints = new();
|
||
|
||
while (continuationPoints.Count > 0)
|
||
{
|
||
// continue browse operation.
|
||
session.BrowseNext(
|
||
null,
|
||
true,
|
||
continuationPoints,
|
||
out results,
|
||
out diagnosticInfos);
|
||
|
||
ClientBase.ValidateResponse(results, continuationPoints);
|
||
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints);
|
||
|
||
for (int ii = 0; ii < continuationPoints.Count; ii++)
|
||
{
|
||
// check for error.
|
||
if (StatusCode.IsBad(results[ii].StatusCode))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// check if all references have been fetched.
|
||
if (results[ii].References.Count == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// save results.
|
||
references.AddRange(results[ii].References);
|
||
|
||
// check for continuation point.
|
||
if (results[ii].ContinuationPoint != null)
|
||
{
|
||
revisedContiuationPoints.Add(results[ii].ContinuationPoint);
|
||
}
|
||
}
|
||
|
||
// check if browsing must continue;
|
||
revisedContiuationPoints = continuationPoints;
|
||
}
|
||
|
||
// check if unprocessed results exist.
|
||
nodesToBrowse = unprocessedOperations;
|
||
}
|
||
|
||
// return complete list.
|
||
return references;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
if (throwOnError)
|
||
{
|
||
throw new ServiceResultException(exception, StatusCodes.BadUnexpectedError);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 浏览地址空间
|
||
/// </summary>
|
||
/// <param name="session"></param>
|
||
/// <param name="nodesToBrowse"></param>
|
||
/// <param name="throwOnError"></param>
|
||
/// <param name="cancellationToken"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ServiceResultException"></exception>
|
||
public static async Task<ReferenceDescriptionCollection> BrowseAsync(ISession session, BrowseDescriptionCollection nodesToBrowse, bool throwOnError, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
ReferenceDescriptionCollection references = new();
|
||
BrowseDescriptionCollection unprocessedOperations = new();
|
||
|
||
while (nodesToBrowse.Count > 0)
|
||
{
|
||
// start the browse operation.
|
||
|
||
var result = await session.BrowseAsync(
|
||
null,
|
||
null,
|
||
0,
|
||
nodesToBrowse, cancellationToken);
|
||
var results = result.Results;
|
||
var diagnosticInfos = result.DiagnosticInfos;
|
||
ClientBase.ValidateResponse(results, nodesToBrowse);
|
||
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse);
|
||
|
||
var continuationPoints = PrepareBrowseNext(result.Results);
|
||
|
||
for (int ii = 0; ii < nodesToBrowse.Count; ii++)
|
||
{
|
||
|
||
// check if all references have been fetched.
|
||
if (results[ii].References.Count == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// check for error.
|
||
if (StatusCode.IsBad(results[ii].StatusCode))
|
||
{
|
||
// this error indicates that the server does not have enough simultaneously active
|
||
// continuation points. This request will need to be resent after the other operations
|
||
// have been completed and their continuation points released.
|
||
if (results[ii].StatusCode == StatusCodes.BadNoContinuationPoints)
|
||
{
|
||
unprocessedOperations.Add(nodesToBrowse[ii]);
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
|
||
|
||
// save results.
|
||
references.AddRange(results[ii].References);
|
||
|
||
}
|
||
|
||
|
||
while (continuationPoints.Any())
|
||
{
|
||
// continue browse operation.
|
||
var nextResult = await session.BrowseNextAsync(
|
||
null,
|
||
false,
|
||
continuationPoints
|
||
, cancellationToken);
|
||
results = nextResult.Results;
|
||
diagnosticInfos = nextResult.DiagnosticInfos;
|
||
ClientBase.ValidateResponse(results, continuationPoints);
|
||
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints);
|
||
|
||
for (int ii = 0; ii < continuationPoints.Count; ii++)
|
||
{
|
||
// check if all references have been fetched.
|
||
if (results[ii].References.Count == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// check for error.
|
||
if (StatusCode.IsBad(results[ii].StatusCode))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
|
||
|
||
// save results.
|
||
references.AddRange(results[ii].References);
|
||
|
||
|
||
}
|
||
|
||
// check if browsing must continue;
|
||
continuationPoints = PrepareBrowseNext(nextResult.Results);
|
||
}
|
||
|
||
// check if unprocessed results exist.
|
||
nodesToBrowse = unprocessedOperations;
|
||
}
|
||
|
||
// return complete list.
|
||
return references;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
if (throwOnError)
|
||
{
|
||
throw new ServiceResultException(exception, StatusCodes.BadUnexpectedError);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 浏览地址空间
|
||
/// </summary>
|
||
/// <param name="session"></param>
|
||
/// <param name="nodeToBrowse"></param>
|
||
/// <param name="throwOnError"></param>
|
||
/// <param name="cancellationToken"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ServiceResultException"></exception>
|
||
public static async Task<ReferenceDescriptionCollection> BrowseAsync(ISession session, BrowseDescription nodeToBrowse, bool throwOnError, CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
ReferenceDescriptionCollection references = new();
|
||
|
||
// construct browse request.
|
||
BrowseDescriptionCollection nodesToBrowse = new()
|
||
{
|
||
nodeToBrowse
|
||
};
|
||
|
||
// start the browse operation.
|
||
|
||
|
||
var result = await session.BrowseAsync(
|
||
null,
|
||
null,
|
||
0,
|
||
nodesToBrowse, cancellationToken);
|
||
var results = result.Results;
|
||
var diagnosticInfos = result.DiagnosticInfos;
|
||
ClientBase.ValidateResponse(results, nodesToBrowse);
|
||
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse);
|
||
|
||
do
|
||
{
|
||
// check for error.
|
||
if (StatusCode.IsBad(results[0].StatusCode))
|
||
{
|
||
throw new ServiceResultException(results[0].StatusCode);
|
||
}
|
||
|
||
// process results.
|
||
for (int ii = 0; ii < results[0].References.Count; ii++)
|
||
{
|
||
references.Add(results[0].References[ii]);
|
||
}
|
||
|
||
// check if all references have been fetched.
|
||
if (results[0].References.Count == 0 || results[0].ContinuationPoint == null)
|
||
{
|
||
break;
|
||
}
|
||
|
||
// continue browse operation.
|
||
ByteStringCollection continuationPoints = new()
|
||
{
|
||
results[0].ContinuationPoint
|
||
};
|
||
|
||
var nextResult = await session.BrowseNextAsync(
|
||
null,
|
||
false,
|
||
continuationPoints, cancellationToken);
|
||
results = nextResult.Results;
|
||
diagnosticInfos = nextResult.DiagnosticInfos;
|
||
ClientBase.ValidateResponse(results, continuationPoints);
|
||
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints);
|
||
}
|
||
while (true);
|
||
|
||
//return complete list.
|
||
return references;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
if (throwOnError)
|
||
{
|
||
throw new ServiceResultException(exception, StatusCodes.BadUnexpectedError);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 浏览地址空间并返回指定类型的所有节点
|
||
/// </summary>
|
||
/// <param name="session"></param>
|
||
/// <param name="typeId"></param>
|
||
/// <param name="throwOnError"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ServiceResultException"></exception>
|
||
public static async Task<ReferenceDescriptionCollection> BrowseSuperTypesAsync(ISession session, NodeId typeId, bool throwOnError)
|
||
{
|
||
ReferenceDescriptionCollection supertypes = new();
|
||
|
||
try
|
||
{
|
||
// find all of the children of the field.
|
||
BrowseDescription nodeToBrowse = new()
|
||
{
|
||
NodeId = typeId,
|
||
BrowseDirection = BrowseDirection.Inverse,
|
||
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
||
IncludeSubtypes = false, // more efficient to use IncludeSubtypes=False when possible.
|
||
NodeClassMask = 0, // the HasSubtype reference already restricts the targets to Types.
|
||
ResultMask = (uint)BrowseResultMask.All
|
||
};
|
||
|
||
ReferenceDescriptionCollection references = await BrowseAsync(session, nodeToBrowse, throwOnError);
|
||
|
||
while (references != null && references.Count > 0)
|
||
{
|
||
// should never be more than one supertype.
|
||
supertypes.Add(references[0]);
|
||
|
||
// only follow references within this server.
|
||
if (references[0].NodeId.IsAbsolute)
|
||
{
|
||
break;
|
||
}
|
||
|
||
// get the references for the next level up.
|
||
nodeToBrowse.NodeId = (NodeId)references[0].NodeId;
|
||
references = await BrowseAsync(session, nodeToBrowse, throwOnError);
|
||
}
|
||
|
||
// return complete list.
|
||
return supertypes;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
if (throwOnError)
|
||
{
|
||
throw new ServiceResultException(exception, StatusCodes.BadUnexpectedError);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Collects the fields for the instance.
|
||
/// </summary>
|
||
public static async Task CollectFieldsForInstanceAsync(ISession session, NodeId instanceId, SimpleAttributeOperandCollection fields, List<NodeId> fieldNodeIds)
|
||
{
|
||
Dictionary<NodeId, QualifiedNameCollection> foundNodes = new();
|
||
QualifiedNameCollection parentPath = new();
|
||
await CollectFieldsAsync(session, instanceId, parentPath, fields, fieldNodeIds, foundNodes);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Collects the fields for the type.
|
||
/// </summary>
|
||
public static async Task CollectFieldsForType(ISession session, NodeId typeId, SimpleAttributeOperandCollection fields, List<NodeId> fieldNodeIds)
|
||
{
|
||
// get the supertypes.
|
||
ReferenceDescriptionCollection supertypes = await FormUtils.BrowseSuperTypesAsync(session, typeId, false);
|
||
|
||
if (supertypes == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// process the types starting from the top of the tree.
|
||
Dictionary<NodeId, QualifiedNameCollection> foundNodes = new();
|
||
QualifiedNameCollection parentPath = new();
|
||
|
||
for (int ii = supertypes.Count - 1; ii >= 0; ii--)
|
||
{
|
||
await CollectFieldsAsync(session, (NodeId)supertypes[ii].NodeId, parentPath, fields, fieldNodeIds, foundNodes);
|
||
}
|
||
|
||
// collect the fields for the selected type.
|
||
await CollectFieldsAsync(session, typeId, parentPath, fields, fieldNodeIds, foundNodes);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Constructs an event object from a notification.
|
||
/// </summary>
|
||
/// <param name="session">The session.</param>
|
||
/// <param name="monitoredItem">The monitored item that produced the notification.</param>
|
||
/// <param name="notification">The notification.</param>
|
||
/// <param name="knownEventTypes">The known event types.</param>
|
||
/// <param name="eventTypeMappings">Mapping between event types and known event types.</param>
|
||
/// <returns>
|
||
/// The event object. Null if the notification is not a valid event type.
|
||
/// </returns>
|
||
public static async Task<BaseEventState> ConstructEventAsync(
|
||
ISession session,
|
||
MonitoredItem monitoredItem,
|
||
EventFieldList notification,
|
||
Dictionary<NodeId, Type> knownEventTypes,
|
||
Dictionary<NodeId, NodeId> eventTypeMappings)
|
||
{
|
||
// find the event type.
|
||
NodeId eventTypeId = FindEventType(monitoredItem, notification);
|
||
|
||
if (eventTypeId == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// look up the known event type.
|
||
Type knownType = null;
|
||
if (eventTypeMappings.TryGetValue(eventTypeId, out NodeId knownTypeId))
|
||
{
|
||
knownType = knownEventTypes[knownTypeId];
|
||
}
|
||
|
||
// try again.
|
||
if (knownType == null)
|
||
{
|
||
if (knownEventTypes.TryGetValue(eventTypeId, out knownType))
|
||
{
|
||
knownTypeId = eventTypeId;
|
||
eventTypeMappings.Add(eventTypeId, eventTypeId);
|
||
}
|
||
}
|
||
|
||
// try mapping it to a known type.
|
||
if (knownType == null)
|
||
{
|
||
// browse for the supertypes of the event type.
|
||
ReferenceDescriptionCollection supertypes = await FormUtils.BrowseSuperTypesAsync(session, eventTypeId, false);
|
||
|
||
// can't do anything with unknown types.
|
||
if (supertypes == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// find the first supertype that matches a known event type.
|
||
for (int ii = 0; ii < supertypes.Count; ii++)
|
||
{
|
||
NodeId superTypeId = (NodeId)supertypes[ii].NodeId;
|
||
|
||
if (knownEventTypes.TryGetValue(superTypeId, out knownType))
|
||
{
|
||
knownTypeId = superTypeId;
|
||
eventTypeMappings.Add(eventTypeId, superTypeId);
|
||
}
|
||
|
||
if (knownTypeId != null)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
// can't do anything with unknown types.
|
||
if (knownTypeId == null)
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// construct the event based on the known event type.
|
||
BaseEventState e = (BaseEventState)Activator.CreateInstance(knownType, new object[] { (NodeState)null });
|
||
|
||
// get the filter which defines the contents of the notification.
|
||
EventFilter filter = monitoredItem.Status.Filter as EventFilter;
|
||
|
||
// initialize the event with the values in the notification.
|
||
e.Update(session.SystemContext, filter.SelectClauses, notification);
|
||
|
||
// save the orginal notification.
|
||
e.Handle = notification;
|
||
|
||
return e;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Discovers the servers on the local machine.
|
||
/// </summary>
|
||
/// <param name="configuration">The configuration.</param>
|
||
/// <returns>A list of server urls.</returns>
|
||
public static IList<string> DiscoverServers(ApplicationConfiguration configuration)
|
||
{
|
||
List<string> serverUrls = new();
|
||
|
||
// set a short timeout because this is happening in the drop down event.
|
||
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(configuration);
|
||
endpointConfiguration.OperationTimeout = 5000;
|
||
|
||
// Connect to the local discovery server and find the available servers.
|
||
using (DiscoveryClient client = DiscoveryClient.Create(new Uri("opc.tcp://localhost:4840"), endpointConfiguration))
|
||
{
|
||
ApplicationDescriptionCollection servers = client.FindServers(null);
|
||
|
||
// populate the drop down list with the discovery URLs for the available servers.
|
||
for (int ii = 0; ii < servers.Count; ii++)
|
||
{
|
||
if (servers[ii].ApplicationType == ApplicationType.DiscoveryServer)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
for (int jj = 0; jj < servers[ii].DiscoveryUrls.Count; jj++)
|
||
{
|
||
string discoveryUrl = servers[ii].DiscoveryUrls[jj];
|
||
|
||
// Many servers will use the '/discovery' suffix for the discovery endpoint.
|
||
// The URL without this prefix should be the base URL for the server.
|
||
if (discoveryUrl.EndsWith("/discovery"))
|
||
{
|
||
discoveryUrl = discoveryUrl.Substring(0, discoveryUrl.Length - "/discovery".Length);
|
||
}
|
||
|
||
// ensure duplicates do not get added.
|
||
if (!serverUrls.Contains(discoveryUrl))
|
||
{
|
||
serverUrls.Add(discoveryUrl);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return serverUrls;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Finds the type of the event for the notification.
|
||
/// </summary>
|
||
/// <param name="monitoredItem">The monitored item.</param>
|
||
/// <param name="notification">The notification.</param>
|
||
/// <returns>The NodeId of the EventType.</returns>
|
||
public static NodeId FindEventType(MonitoredItem monitoredItem, EventFieldList notification)
|
||
{
|
||
if (monitoredItem.Status.Filter is EventFilter filter)
|
||
{
|
||
for (int ii = 0; ii < filter.SelectClauses.Count; ii++)
|
||
{
|
||
SimpleAttributeOperand clause = filter.SelectClauses[ii];
|
||
|
||
if (clause.BrowsePath.Count == 1 && clause.BrowsePath[0] == BrowseNames.EventType)
|
||
{
|
||
return notification.EventFields[ii].Value as NodeId;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 指定的属性的显示文本。
|
||
/// </summary>
|
||
public static string GetAttributeDisplayText(ISession session, uint attributeId, Variant value)
|
||
{
|
||
if (value == Variant.Null)
|
||
{
|
||
return String.Empty;
|
||
}
|
||
|
||
switch (attributeId)
|
||
{
|
||
case Attributes.AccessLevel:
|
||
case Attributes.UserAccessLevel:
|
||
{
|
||
byte? field = value.Value as byte?;
|
||
|
||
if (field != null)
|
||
{
|
||
return GetAccessLevelDisplayText(field.Value);
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case Attributes.EventNotifier:
|
||
{
|
||
byte? field = value.Value as byte?;
|
||
|
||
if (field != null)
|
||
{
|
||
return GetEventNotifierDisplayText(field.Value);
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case Attributes.DataType:
|
||
{
|
||
return session.NodeCache.GetDisplayText(value.Value as NodeId);
|
||
}
|
||
|
||
case Attributes.ValueRank:
|
||
{
|
||
int? field = value.Value as int?;
|
||
|
||
if (field != null)
|
||
{
|
||
return GetValueRankDisplayText(field.Value);
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case Attributes.NodeClass:
|
||
{
|
||
int? field = value.Value as int?;
|
||
|
||
if (field != null)
|
||
{
|
||
return ((NodeClass)field.Value).ToString();
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case Attributes.NodeId:
|
||
{
|
||
NodeId field = value.Value as NodeId;
|
||
|
||
if (!NodeId.IsNull(field))
|
||
{
|
||
return field.ToString();
|
||
}
|
||
|
||
return "Null";
|
||
}
|
||
}
|
||
|
||
// check for byte strings.
|
||
if (value.Value is byte[])
|
||
{
|
||
return Utils.ToHexString(value.Value as byte[]);
|
||
}
|
||
|
||
// use default format.
|
||
return value.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Finds the endpoint that best matches the current settings.
|
||
/// </summary>
|
||
/// <param name="discoveryUrl">The discovery URL.</param>
|
||
/// <param name="useSecurity">if set to <c>true</c> select an endpoint that uses security.</param>
|
||
/// <returns>The best available endpoint.</returns>
|
||
public static EndpointDescription SelectEndpoint(string discoveryUrl, bool useSecurity)
|
||
{
|
||
// needs to add the '/discovery' back onto non-UA TCP URLs.
|
||
if (!discoveryUrl.StartsWith(Utils.UriSchemeOpcTcp))
|
||
{
|
||
if (!discoveryUrl.EndsWith("/discovery"))
|
||
{
|
||
discoveryUrl += "/discovery";
|
||
}
|
||
}
|
||
|
||
// parse the selected URL.
|
||
Uri uri = new(discoveryUrl);
|
||
|
||
// set a short timeout because this is happening in the drop down event.
|
||
EndpointConfiguration configuration = EndpointConfiguration.Create();
|
||
configuration.OperationTimeout = 5000;
|
||
|
||
EndpointDescription selectedEndpoint = null;
|
||
|
||
// Connect to the server's discovery endpoint and find the available configuration.
|
||
using (DiscoveryClient client = DiscoveryClient.Create(uri, configuration))
|
||
{
|
||
EndpointDescriptionCollection endpoints = client.GetEndpoints(null);
|
||
|
||
// select the best endpoint to use based on the selected URL and the UseSecurity checkbox.
|
||
for (int ii = 0; ii < endpoints.Count; ii++)
|
||
{
|
||
EndpointDescription endpoint = endpoints[ii];
|
||
|
||
// check for a match on the URL scheme.
|
||
if (endpoint.EndpointUrl.StartsWith(uri.Scheme))
|
||
{
|
||
// check if security was requested.
|
||
if (useSecurity)
|
||
{
|
||
if (endpoint.SecurityMode == MessageSecurityMode.None)
|
||
{
|
||
continue;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (endpoint.SecurityMode != MessageSecurityMode.None)
|
||
{
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// pick the first available endpoint by default.
|
||
selectedEndpoint ??= endpoint;
|
||
|
||
// The security level is a relative measure assigned by the server to the
|
||
// endpoints that it returns. Clients should always pick the highest level
|
||
// unless they have a reason not too.
|
||
if (endpoint.SecurityLevel > selectedEndpoint.SecurityLevel)
|
||
{
|
||
selectedEndpoint = endpoint;
|
||
}
|
||
}
|
||
}
|
||
|
||
// pick the first available endpoint by default.
|
||
if (selectedEndpoint == null && endpoints.Count > 0)
|
||
{
|
||
selectedEndpoint = endpoints[0];
|
||
}
|
||
}
|
||
|
||
// if a server is behind a firewall it may return URLs that are not accessible to the client.
|
||
// This problem can be avoided by assuming that the domain in the URL used to call
|
||
// GetEndpoints can be used to access any of the endpoints. This code makes that conversion.
|
||
// Note that the conversion only makes sense if discovery uses the same protocol as the endpoint.
|
||
|
||
Uri endpointUrl = Utils.ParseUri(selectedEndpoint.EndpointUrl);
|
||
|
||
if (endpointUrl != null && endpointUrl.Scheme == uri.Scheme)
|
||
{
|
||
UriBuilder builder = new(endpointUrl)
|
||
{
|
||
Host = uri.DnsSafeHost,
|
||
Port = uri.Port
|
||
};
|
||
selectedEndpoint.EndpointUrl = builder.ToString();
|
||
}
|
||
|
||
// return the selected endpoint.
|
||
return selectedEndpoint;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回一组相对路径的节点id
|
||
/// </summary>
|
||
public static async Task<List<NodeId>> TranslateBrowsePaths(
|
||
ISession session,
|
||
NodeId startNodeId,
|
||
NamespaceTable namespacesUris, CancellationToken cancellationToken,
|
||
params string[] relativePaths)
|
||
{
|
||
// build the list of browse paths to follow by parsing the relative paths.
|
||
BrowsePathCollection browsePaths = new();
|
||
|
||
if (relativePaths != null)
|
||
{
|
||
for (int ii = 0; ii < relativePaths.Length; ii++)
|
||
{
|
||
BrowsePath browsePath = new()
|
||
{
|
||
RelativePath = RelativePath.Parse(
|
||
relativePaths[ii],
|
||
session.TypeTree,
|
||
namespacesUris,
|
||
session.NamespaceUris),
|
||
|
||
StartingNode = startNodeId
|
||
};
|
||
|
||
browsePaths.Add(browsePath);
|
||
}
|
||
}
|
||
|
||
// make the call to the server.
|
||
|
||
|
||
var result = await session.TranslateBrowsePathsToNodeIdsAsync(
|
||
null,
|
||
browsePaths,
|
||
cancellationToken);
|
||
BrowsePathResultCollection results = result.Results;
|
||
DiagnosticInfoCollection diagnosticInfos = result.DiagnosticInfos;
|
||
// ensure that the server returned valid results.
|
||
ClientBase.ValidateResponse(results, browsePaths);
|
||
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, browsePaths);
|
||
|
||
// collect the list of node ids found.
|
||
List<NodeId> nodes = new();
|
||
|
||
for (int ii = 0; ii < results.Count; ii++)
|
||
{
|
||
// check if the start node actually exists.
|
||
if (StatusCode.IsBad(results[ii].StatusCode))
|
||
{
|
||
nodes.Add(null);
|
||
continue;
|
||
}
|
||
|
||
// an empty list is returned if no node was found.
|
||
if (results[ii].Targets.Count == 0)
|
||
{
|
||
nodes.Add(null);
|
||
continue;
|
||
}
|
||
|
||
// Multiple matches are possible, however, the node that matches the type model is the
|
||
// one we are interested in here. The rest can be ignored.
|
||
BrowsePathTarget target = results[ii].Targets[0];
|
||
|
||
if (target.RemainingPathIndex != UInt32.MaxValue)
|
||
{
|
||
nodes.Add(null);
|
||
continue;
|
||
}
|
||
|
||
// The targetId is an ExpandedNodeId because it could be node in another server.
|
||
// The ToNodeId function is used to convert a local NodeId stored in a ExpandedNodeId to a NodeId.
|
||
nodes.Add(ExpandedNodeId.ToNodeId(target.TargetId, session.NamespaceUris));
|
||
}
|
||
|
||
// return whatever was found.
|
||
return nodes;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Collects the fields for the instance node.
|
||
/// </summary>
|
||
/// <param name="session">The session.</param>
|
||
/// <param name="nodeId">The node id.</param>
|
||
/// <param name="parentPath">The parent path.</param>
|
||
/// <param name="fields">The event fields.</param>
|
||
/// <param name="fieldNodeIds">The node id for the declaration of the field.</param>
|
||
/// <param name="foundNodes">The table of found nodes.</param>
|
||
private static async Task CollectFieldsAsync(
|
||
ISession session,
|
||
NodeId nodeId,
|
||
QualifiedNameCollection parentPath,
|
||
SimpleAttributeOperandCollection fields,
|
||
List<NodeId> fieldNodeIds,
|
||
Dictionary<NodeId, QualifiedNameCollection> foundNodes)
|
||
{
|
||
// find all of the children of the field.
|
||
BrowseDescription nodeToBrowse = new()
|
||
{
|
||
NodeId = nodeId,
|
||
BrowseDirection = BrowseDirection.Forward,
|
||
ReferenceTypeId = ReferenceTypeIds.Aggregates,
|
||
IncludeSubtypes = true,
|
||
NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable),
|
||
ResultMask = (uint)BrowseResultMask.All
|
||
};
|
||
|
||
ReferenceDescriptionCollection children = await FormUtils.BrowseAsync(session, nodeToBrowse, false);
|
||
|
||
if (children == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// process the children.
|
||
for (int ii = 0; ii < children.Count; ii++)
|
||
{
|
||
ReferenceDescription child = children[ii];
|
||
|
||
if (child.NodeId.IsAbsolute)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// construct browse path.
|
||
QualifiedNameCollection browsePath = new(parentPath)
|
||
{
|
||
child.BrowseName
|
||
};
|
||
|
||
// check if the browse path is already in the list.
|
||
int index = ContainsPath(fields, browsePath);
|
||
|
||
if (index < 0)
|
||
{
|
||
SimpleAttributeOperand field = new()
|
||
{
|
||
TypeDefinitionId = ObjectTypeIds.BaseEventType,
|
||
BrowsePath = browsePath,
|
||
AttributeId = (child.NodeClass == NodeClass.Variable) ? Attributes.Value : Attributes.NodeId
|
||
};
|
||
|
||
fields.Add(field);
|
||
fieldNodeIds.Add((NodeId)child.NodeId);
|
||
}
|
||
|
||
// recusively find all of the children.
|
||
NodeId targetId = (NodeId)child.NodeId;
|
||
|
||
// need to guard against loops.
|
||
if (!foundNodes.ContainsKey(targetId))
|
||
{
|
||
foundNodes.Add(targetId, browsePath);
|
||
await CollectFieldsAsync(session, (NodeId)child.NodeId, browsePath, fields, fieldNodeIds, foundNodes);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断指定的select子句包含的浏览路径。
|
||
/// </summary>
|
||
private static int ContainsPath(SimpleAttributeOperandCollection selectClause, QualifiedNameCollection browsePath)
|
||
{
|
||
for (int ii = 0; ii < selectClause.Count; ii++)
|
||
{
|
||
SimpleAttributeOperand field = selectClause[ii];
|
||
|
||
if (field.BrowsePath.Count != browsePath.Count)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
bool match = true;
|
||
|
||
for (int jj = 0; jj < field.BrowsePath.Count; jj++)
|
||
{
|
||
if (field.BrowsePath[jj] != browsePath[jj])
|
||
{
|
||
match = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (match)
|
||
{
|
||
return ii;
|
||
}
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
/// <summary>
|
||
///访问级别属性的显示文本。
|
||
/// </summary>
|
||
private static string GetAccessLevelDisplayText(byte accessLevel)
|
||
{
|
||
StringBuilder buffer = new();
|
||
|
||
if (accessLevel == AccessLevels.None)
|
||
{
|
||
buffer.Append("None");
|
||
}
|
||
|
||
if ((accessLevel & AccessLevels.CurrentRead) == AccessLevels.CurrentRead)
|
||
{
|
||
buffer.Append("Read");
|
||
}
|
||
|
||
if ((accessLevel & AccessLevels.CurrentWrite) == AccessLevels.CurrentWrite)
|
||
{
|
||
if (buffer.Length > 0)
|
||
{
|
||
buffer.Append(" | ");
|
||
}
|
||
|
||
buffer.Append("Write");
|
||
}
|
||
|
||
if ((accessLevel & AccessLevels.HistoryRead) == AccessLevels.HistoryRead)
|
||
{
|
||
if (buffer.Length > 0)
|
||
{
|
||
buffer.Append(" | ");
|
||
}
|
||
|
||
buffer.Append("HistoryRead");
|
||
}
|
||
|
||
if ((accessLevel & AccessLevels.HistoryWrite) == AccessLevels.HistoryWrite)
|
||
{
|
||
if (buffer.Length > 0)
|
||
{
|
||
buffer.Append(" | ");
|
||
}
|
||
|
||
buffer.Append("HistoryWrite");
|
||
}
|
||
|
||
if ((accessLevel & AccessLevels.SemanticChange) == AccessLevels.SemanticChange)
|
||
{
|
||
if (buffer.Length > 0)
|
||
{
|
||
buffer.Append(" | ");
|
||
}
|
||
|
||
buffer.Append("SemanticChange");
|
||
}
|
||
|
||
return buffer.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 事件通知属性的显示文本
|
||
/// </summary>
|
||
private static string GetEventNotifierDisplayText(byte eventNotifier)
|
||
{
|
||
StringBuilder buffer = new();
|
||
|
||
if (eventNotifier == EventNotifiers.None)
|
||
{
|
||
buffer.Append("None");
|
||
}
|
||
|
||
if ((eventNotifier & EventNotifiers.SubscribeToEvents) == EventNotifiers.SubscribeToEvents)
|
||
{
|
||
buffer.Append("Subscribe");
|
||
}
|
||
|
||
if ((eventNotifier & EventNotifiers.HistoryRead) == EventNotifiers.HistoryRead)
|
||
{
|
||
if (buffer.Length > 0)
|
||
{
|
||
buffer.Append(" | ");
|
||
}
|
||
|
||
buffer.Append("HistoryRead");
|
||
}
|
||
|
||
if ((eventNotifier & EventNotifiers.HistoryWrite) == EventNotifiers.HistoryWrite)
|
||
{
|
||
if (buffer.Length > 0)
|
||
{
|
||
buffer.Append(" | ");
|
||
}
|
||
|
||
buffer.Append("HistoryWrite");
|
||
}
|
||
|
||
return buffer.ToString();
|
||
}
|
||
|
||
private static string GetValueRankDisplayText(int valueRank)
|
||
{
|
||
return valueRank switch
|
||
{
|
||
ValueRanks.Any => "Any",
|
||
ValueRanks.Scalar => "Scalar",
|
||
ValueRanks.ScalarOrOneDimension => "ScalarOrOneDimension",
|
||
ValueRanks.OneOrMoreDimensions => "OneOrMoreDimensions",
|
||
ValueRanks.OneDimension => "OneDimension",
|
||
ValueRanks.TwoDimensions => "TwoDimensions",
|
||
_ => valueRank.ToString(),
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Create the continuation point collection from the browse result
|
||
/// collection for the BrowseNext service.
|
||
/// </summary>
|
||
/// <param name="browseResultCollection">The browse result collection to use.</param>
|
||
/// <returns>The collection of continuation points for the BrowseNext service.</returns>
|
||
private static ByteStringCollection PrepareBrowseNext(BrowseResultCollection browseResultCollection)
|
||
{
|
||
var continuationPoints = new ByteStringCollection();
|
||
foreach (var browseResult in browseResultCollection)
|
||
{
|
||
if (browseResult.ContinuationPoint != null)
|
||
{
|
||
continuationPoints.Add(browseResult.ContinuationPoint);
|
||
}
|
||
}
|
||
return continuationPoints;
|
||
}
|
||
}
|