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; } } }