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 { /// /// 辅助类 /// public class FormUtils { /// /// Gets the display text for the access level attribute. /// /// The access level. /// The access level formatted as a string. 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(); } /// /// Gets the display text for the event notifier attribute. /// /// The event notifier. /// The event notifier formatted as a string. 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(); } /// /// Gets the display text for the value rank attribute. /// /// The value rank. /// The value rank formatted as a string. 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(); } /// /// Gets the display text for the specified attribute. /// /// The currently active session. /// The id of the attribute. /// The value of the attribute. /// The attribute formatted as a string. 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(); } /// /// Discovers the servers on the local machine. /// /// The configuration. /// A list of server urls. public static IList DiscoverServers(ApplicationConfiguration configuration) { List serverUrls = new List(); // 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; } /// /// Finds the endpoint that best matches the current settings. /// /// The discovery URL. /// if set to true select an endpoint that uses security. /// The best available endpoint. 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; } /// /// Browses the address space and returns the references found. /// /// The session. /// The set of browse operations to perform. /// if set to true a exception will be thrown on an error. /// /// The references found. Null if an error occurred. /// 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; } } /// /// Finds the type of the event for the notification. /// /// The monitored item. /// The notification. /// The NodeId of the EventType. 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; } /// /// Browses the address space and returns the references found. /// /// The session. /// The NodeId for the starting node. /// if set to true a exception will be thrown on an error. /// /// The references found. Null if an error occurred. /// 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; } } /// /// Browses the address space and returns all of the supertypes of the specified type node. /// /// The session. /// The NodeId for a type node in the address space. /// if set to true a exception will be thrown on an error. /// /// The references found. Null if an error occurred. /// 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; } } /// /// Constructs an event object from a notification. /// /// The session. /// The monitored item that produced the notification. /// The notification. /// The known event types. /// Mapping between event types and known event types. /// /// The event object. Null if the notification is not a valid event type. /// public static BaseEventState ConstructEvent( Session session, MonitoredItem monitoredItem, EventFieldList notification, Dictionary knownEventTypes, Dictionary 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; } /// /// Returns the node ids for a set of relative paths. /// /// An open session with the server to use. /// The starting node for the relative paths. /// The namespace URIs referenced by the relative paths. /// The relative paths. /// A collection of local nodes. public static List 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 nodes = new List(); 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; } /// /// Collects the fields for the type. /// /// The session. /// The fields. /// The node id for the declaration of the field. public static void CollectFieldsForType(Session session, NodeId typeId, SimpleAttributeOperandCollection fields, List 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 foundNodes = new Dictionary(); 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); } /// /// Collects the fields for the instance. /// /// The session. /// The fields. /// The node id for the declaration of the field. public static void CollectFieldsForInstance(Session session, NodeId instanceId, SimpleAttributeOperandCollection fields, List fieldNodeIds) { Dictionary foundNodes = new Dictionary(); QualifiedNameCollection parentPath = new QualifiedNameCollection(); CollectFields(session, instanceId, parentPath, fields, fieldNodeIds, foundNodes); } /// /// Collects the fields for the instance node. /// /// The session. /// The node id. /// The parent path. /// The event fields. /// The node id for the declaration of the field. /// The table of found nodes. private static void CollectFields( Session session, NodeId nodeId, QualifiedNameCollection parentPath, SimpleAttributeOperandCollection fields, List fieldNodeIds, Dictionary 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); } } } /// /// Determines whether the specified select clause contains the browse path. /// /// The select clause. /// The browse path. /// /// true if the specified select clause contains path; otherwise, false. /// 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; } } }