Files
2025-08-26 08:37:44 +08:00

1022 lines
39 KiB
C#

using Opc.Ua;
using Opc.Ua.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpcUaHelper
{
/// <summary>
/// 辅助类
/// </summary>
public class FormUtils
{
/// <summary>
/// Gets the display text for the access level attribute.
/// </summary>
/// <param name="accessLevel">The access level.</param>
/// <returns>The access level formatted as a string.</returns>
private static string GetAccessLevelDisplayText(byte accessLevel)
{
StringBuilder buffer = new StringBuilder();
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>
/// Gets the display text for the event notifier attribute.
/// </summary>
/// <param name="eventNotifier">The event notifier.</param>
/// <returns>The event notifier formatted as a string.</returns>
private static string GetEventNotifierDisplayText(byte eventNotifier)
{
StringBuilder buffer = new StringBuilder();
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();
}
/// <summary>
/// Gets the display text for the value rank attribute.
/// </summary>
/// <param name="valueRank">The value rank.</param>
/// <returns>The value rank formatted as a string.</returns>
private static string GetValueRankDisplayText(int valueRank)
{
switch (valueRank)
{
case ValueRanks.Any: return "Any";
case ValueRanks.Scalar: return "Scalar";
case ValueRanks.ScalarOrOneDimension: return "ScalarOrOneDimension";
case ValueRanks.OneOrMoreDimensions: return "OneOrMoreDimensions";
case ValueRanks.OneDimension: return "OneDimension";
case ValueRanks.TwoDimensions: return "TwoDimensions";
}
return valueRank.ToString();
}
/// <summary>
/// Gets the display text for the specified attribute.
/// </summary>
/// <param name="session">The currently active session.</param>
/// <param name="attributeId">The id of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
/// <returns>The attribute formatted as a string.</returns>
public static string GetAttributeDisplayText(Session 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>
/// 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 List<string>();
// 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 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 Uri(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.
if (selectedEndpoint == null)
{
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 UriBuilder(endpointUrl);
builder.Host = uri.DnsSafeHost;
builder.Port = uri.Port;
selectedEndpoint.EndpointUrl = builder.ToString();
}
// return the selected endpoint.
return selectedEndpoint;
}
/// <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(Session session, BrowseDescriptionCollection nodesToBrowse, bool throwOnError)
{
try
{
ReferenceDescriptionCollection references = new ReferenceDescriptionCollection();
BrowseDescriptionCollection unprocessedOperations = new BrowseDescriptionCollection();
while (nodesToBrowse.Count > 0)
{
// start the browse operation.
BrowseResultCollection results = null;
DiagnosticInfoCollection diagnosticInfos = null;
session.Browse(
null,
null,
0,
nodesToBrowse,
out results,
out diagnosticInfos);
ClientBase.ValidateResponse(results, nodesToBrowse);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse);
ByteStringCollection continuationPoints = new ByteStringCollection();
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 ByteStringCollection();
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>
/// 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)
{
EventFilter filter = monitoredItem.Status.Filter as EventFilter;
if (filter != null)
{
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>
/// Browses the address space and returns the references found.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="nodeToBrowse">The NodeId for the starting node.</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(Session session, BrowseDescription nodeToBrowse, bool throwOnError)
{
try
{
ReferenceDescriptionCollection references = new ReferenceDescriptionCollection();
// construct browse request.
BrowseDescriptionCollection nodesToBrowse = new BrowseDescriptionCollection();
nodesToBrowse.Add(nodeToBrowse);
// start the browse operation.
BrowseResultCollection results = null;
DiagnosticInfoCollection diagnosticInfos = null;
session.Browse(
null,
null,
0,
nodesToBrowse,
out results,
out 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 ByteStringCollection();
continuationPoints.Add(results[0].ContinuationPoint);
session.BrowseNext(
null,
false,
continuationPoints,
out results,
out 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>
/// Browses the address space and returns all of the supertypes of the specified type node.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="typeId">The NodeId for a type node in the address space.</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 BrowseSuperTypes(Session session, NodeId typeId, bool throwOnError)
{
ReferenceDescriptionCollection supertypes = new ReferenceDescriptionCollection();
try
{
// find all of the children of the field.
BrowseDescription nodeToBrowse = new BrowseDescription();
nodeToBrowse.NodeId = typeId;
nodeToBrowse.BrowseDirection = BrowseDirection.Inverse;
nodeToBrowse.ReferenceTypeId = ReferenceTypeIds.HasSubtype;
nodeToBrowse.IncludeSubtypes = false; // more efficient to use IncludeSubtypes=False when possible.
nodeToBrowse.NodeClassMask = 0; // the HasSubtype reference already restricts the targets to Types.
nodeToBrowse.ResultMask = (uint)BrowseResultMask.All;
ReferenceDescriptionCollection references = Browse(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 = Browse(session, nodeToBrowse, throwOnError);
}
// return complete list.
return supertypes;
}
catch (Exception exception)
{
if (throwOnError)
{
throw new ServiceResultException(exception, StatusCodes.BadUnexpectedError);
}
return null;
}
}
/// <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 BaseEventState ConstructEvent(
Session 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;
NodeId knownTypeId = null;
if (eventTypeMappings.TryGetValue(eventTypeId, out 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 = FormUtils.BrowseSuperTypes(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>
/// Returns the node ids for a set of relative paths.
/// </summary>
/// <param name="session">An open session with the server to use.</param>
/// <param name="startNodeId">The starting node for the relative paths.</param>
/// <param name="namespacesUris">The namespace URIs referenced by the relative paths.</param>
/// <param name="relativePaths">The relative paths.</param>
/// <returns>A collection of local nodes.</returns>
public static List<NodeId> TranslateBrowsePaths(
Session session,
NodeId startNodeId,
NamespaceTable namespacesUris,
params string[] relativePaths)
{
// build the list of browse paths to follow by parsing the relative paths.
BrowsePathCollection browsePaths = new BrowsePathCollection();
if (relativePaths != null)
{
for (int ii = 0; ii < relativePaths.Length; ii++)
{
BrowsePath browsePath = new BrowsePath();
// The relative paths used indexes in the namespacesUris table. These must be
// converted to indexes used by the server. An error occurs if the relative path
// refers to a namespaceUri that the server does not recognize.
// The relative paths may refer to ReferenceType by their BrowseName. The TypeTree object
// allows the parser to look up the server's NodeId for the ReferenceType.
browsePath.RelativePath = RelativePath.Parse(
relativePaths[ii],
session.TypeTree,
namespacesUris,
session.NamespaceUris);
browsePath.StartingNode = startNodeId;
browsePaths.Add(browsePath);
}
}
// make the call to the server.
BrowsePathResultCollection results;
DiagnosticInfoCollection diagnosticInfos;
ResponseHeader responseHeader = session.TranslateBrowsePathsToNodeIds(
null,
browsePaths,
out results,
out diagnosticInfos);
// ensure that the server returned valid results.
Session.ValidateResponse(results, browsePaths);
Session.ValidateDiagnosticInfos(diagnosticInfos, browsePaths);
// collect the list of node ids found.
List<NodeId> nodes = new List<NodeId>();
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 type.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="fields">The fields.</param>
/// <param name="fieldNodeIds">The node id for the declaration of the field.</param>
public static void CollectFieldsForType(Session session, NodeId typeId, SimpleAttributeOperandCollection fields, List<NodeId> fieldNodeIds)
{
// get the supertypes.
ReferenceDescriptionCollection supertypes = FormUtils.BrowseSuperTypes(session, typeId, false);
if (supertypes == null)
{
return;
}
// process the types starting from the top of the tree.
Dictionary<NodeId, QualifiedNameCollection> foundNodes = new Dictionary<NodeId, QualifiedNameCollection>();
QualifiedNameCollection parentPath = new QualifiedNameCollection();
for (int ii = supertypes.Count - 1; ii >= 0; ii--)
{
CollectFields(session, (NodeId)supertypes[ii].NodeId, parentPath, fields, fieldNodeIds, foundNodes);
}
// collect the fields for the selected type.
CollectFields(session, typeId, parentPath, fields, fieldNodeIds, foundNodes);
}
/// <summary>
/// Collects the fields for the instance.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="fields">The fields.</param>
/// <param name="instanceId">The node id for the declaration of the field.</param>
public static void CollectFieldsForInstance(Session session, NodeId instanceId, SimpleAttributeOperandCollection fields, List<NodeId> fieldNodeIds)
{
Dictionary<NodeId, QualifiedNameCollection> foundNodes = new Dictionary<NodeId, QualifiedNameCollection>();
QualifiedNameCollection parentPath = new QualifiedNameCollection();
CollectFields(session, instanceId, parentPath, fields, fieldNodeIds, foundNodes);
}
/// <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 void CollectFields(
Session 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 BrowseDescription();
nodeToBrowse.NodeId = nodeId;
nodeToBrowse.BrowseDirection = BrowseDirection.Forward;
nodeToBrowse.ReferenceTypeId = ReferenceTypeIds.Aggregates;
nodeToBrowse.IncludeSubtypes = true;
nodeToBrowse.NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable);
nodeToBrowse.ResultMask = (uint)BrowseResultMask.All;
ReferenceDescriptionCollection children = FormUtils.Browse(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 QualifiedNameCollection(parentPath);
browsePath.Add(child.BrowseName);
// check if the browse path is already in the list.
int index = ContainsPath(fields, browsePath);
if (index < 0)
{
SimpleAttributeOperand field = new SimpleAttributeOperand();
field.TypeDefinitionId = ObjectTypeIds.BaseEventType;
field.BrowsePath = browsePath;
field.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);
CollectFields(session, (NodeId)child.NodeId, browsePath, fields, fieldNodeIds, foundNodes);
}
}
}
/// <summary>
/// Determines whether the specified select clause contains the browse path.
/// </summary>
/// <param name="selectClause">The select clause.</param>
/// <param name="browsePath">The browse path.</param>
/// <returns>
/// <c>true</c> if the specified select clause contains path; otherwise, <c>false</c>.
/// </returns>
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;
}
}
}