/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Reflection; using System.Threading; using Microsoft.Extensions.Logging; using Opc.Ua.Server; using Quickstarts.Servers; namespace Opc.Ua.Sample { /// /// A node manager for a variety of test data. /// public class SampleNodeManager : INodeManager, INodeIdFactory, IDisposable { /// /// Initializes the node manager. /// public SampleNodeManager(IServerInternal server) { // save a reference to the server that owns the node manager. Server = server; m_logger = server.Telemetry.CreateLogger(); // create the default context. SystemContext = Server.DefaultSystemContext.Copy(); SystemContext.SystemHandle = null; SystemContext.NodeIdFactory = this; // create the table of nodes. m_minimumSamplingInterval = 100; } /// /// Frees any unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// An overrideable version of the Dispose. /// protected virtual void Dispose(bool disposing) { if (disposing) { lock (Lock) { m_samplingTimer?.Dispose(); m_samplingTimer = null; // foreach (NodeState node in PredefinedNodes.Values) // { // node?.Delete(); // } } } } /// /// Mints NodeIds for nodes this NodeManager creates at runtime. /// /// /// This sample base predates and /// so carries the factory itself. A subclass selects its identifier /// style by assigning this rather than by overriding /// . /// public IRebasableNodeIdFactory NodeIdFactory { get; set; } = new DefaultNodeIdFactory(NodeIdAssignmentMode.String); /// /// Creates the NodeId for the specified node. /// /// The context. /// The node. /// The new NodeId. public virtual NodeId New(ISystemContext context, NodeState node) { return NodeIdFactory.New(context, node); } /// /// Acquires the lock on the node manager. /// public object Lock { get; } = new object(); /// /// The server that the node manager belongs to. /// protected IServerInternal Server { get; } /// /// The default context to use. /// protected ServerSystemContext SystemContext { get; } /// /// The predefined nodes managed by the node manager. /// protected NodeIdDictionary PredefinedNodes { get; } = []; /// /// The root notifiers for the node manager. /// protected List RootNotifiers { get; } = []; /// /// Returns true if the namespace for the node id is one of the namespaces managed by the node manager. /// /// The node id to check. /// True if the namespace is one of the nodes. protected virtual bool IsNodeIdInNamespace(NodeId nodeId) { if (nodeId.IsNull) { return false; } // quickly exclude nodes that not in the namespace. for (int ii = 0; ii < m_namespaceIndexes.Length; ii++) { if (nodeId.NamespaceIndex == m_namespaceIndexes[ii]) { return true; } } return false; } /// /// Returns the node if the handle refers to a node managed by this manager. /// /// The handle to check. /// Non-null if the handle belongs to the node manager. protected virtual NodeState? IsHandleInNamespace(object managerHandle) { if (managerHandle is not NodeState source) { return null; } if (!IsNodeIdInNamespace(source.NodeId)) { return null; } return source; } /// /// Returns the state object for the specified node if it exists. /// public NodeState? Find(NodeId nodeId) { lock (Lock) { if (!PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return null; } return node; } } /// /// Creates a new instance and assigns unique identifiers to all children. /// /// The operation context. /// An optional parent identifier. /// The reference type from the parent. /// The browse name. /// The instance to create. /// The new node id. /// public NodeId CreateNode( ServerSystemContext context, NodeId parentId, NodeId referenceTypeId, QualifiedName browseName, BaseInstanceState instance) { ServerSystemContext contextToUse = SystemContext.Copy(context); lock (Lock) { instance.ReferenceTypeId = referenceTypeId; NodeState? parent = null; if (!parentId.IsNull) { if (!PredefinedNodes.TryGetValue(parentId, out parent)) { throw ServiceResultException.Create( StatusCodes.BadNodeIdUnknown, "Cannot find parent with id: {0}", parentId); } parent.AddChild(instance); } instance.Create(contextToUse, default, browseName, default, true); AddPredefinedNode(contextToUse, instance); return instance.NodeId; } } /// /// Deletes a node and all of its children. /// public bool DeleteNode(ServerSystemContext context, NodeId nodeId) { ServerSystemContext contextToUse = SystemContext.Copy(context); bool found = false; var referencesToRemove = new List(); lock (Lock) { if (PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { RemovePredefinedNode(contextToUse, node, referencesToRemove); found = true; } RemoveRootNotifier(node); } // must release the lock before removing cross references to other node managers. if (referencesToRemove.Count > 0) { #pragma warning disable CS0618 // sync RemoveReferences obsolete; this Quickstart still uses sync DeleteNode flow. Server.NodeManager.RemoveReferences(referencesToRemove); #pragma warning restore CS0618 } return found; } /// /// Returns the namespaces used by the node manager. /// /// /// All NodeIds exposed by the node manager must be qualified by a namespace URI. This property /// returns the URIs used by the node manager. In this example all NodeIds use a single URI. /// public virtual IEnumerable NamespaceUris { get => m_namespaceUris; protected set { if (value != null) { m_namespaceUris = [.. value]; } else { m_namespaceUris = []; } m_namespaceIndexes = new ushort[m_namespaceUris.Count]; } } /// /// Does any initialization required before the address space can be used. /// /// /// The externalReferences is an out parameter that allows the node manager to link to nodes /// in other node managers. For example, the 'Objects' node is managed by the CoreNodeManager and /// should have a reference to the root folder node(s) exposed by this node manager. /// public virtual void CreateAddressSpace( IDictionary> externalReferences) { lock (Lock) { // add the uris to the server's namespace table and cache the indexes. for (int ii = 0; ii < m_namespaceUris.Count; ii++) { m_namespaceIndexes[ii] = Server.NamespaceUris .GetIndexOrAppend(m_namespaceUris[ii]); } // A factory still pointing at namespace 0 adopts the first // namespace this manager owns. Namespace 0 is the OPC UA // namespace, so minting there would hand out identifiers in // a namespace no NodeManager owns. A subclass that wants a // different one of its namespaces rebases the factory itself, // as BoilerNodeManager and MemoryBufferNodeManager do. if (NodeIdFactory.DefaultNamespaceIndex == 0 && m_namespaceIndexes.Length > 0) { NodeIdFactory = NodeIdFactory .WithDefaultNamespaceIndex(m_namespaceIndexes[0]); } LoadPredefinedNodes(SystemContext, externalReferences); } } /// /// Loads a node set from a file or resource and adds them to the set of predefined nodes. /// public virtual void LoadPredefinedNodes( ISystemContext context, Assembly assembly, string resourcePath, IDictionary> externalReferences) { // load the predefined nodes from an XML document. var predefinedNodes = new NodeStateCollection(); predefinedNodes.LoadFromResource(context, resourcePath, assembly, true); // add the predefined nodes to the node manager. for (int ii = 0; ii < predefinedNodes.Count; ii++) { AddPredefinedNode(context, predefinedNodes[ii]); } // ensure the reverse references exist. AddReverseReferences(externalReferences); } /// /// Loads a node set from a file or resource and adds them to the set of predefined nodes. /// protected virtual NodeStateCollection LoadPredefinedNodes(ISystemContext context) { return []; } /// /// Loads a node set from a file or resource and adds them to the set of predefined nodes. /// protected virtual void LoadPredefinedNodes( ISystemContext context, IDictionary> externalReferences) { // load the predefined nodes from an XML document. NodeStateCollection predefinedNodes = LoadPredefinedNodes(context); // add the predefined nodes to the node manager. for (int ii = 0; ii < predefinedNodes.Count; ii++) { AddPredefinedNode(context, predefinedNodes[ii]); } // ensure the reverse references exist. AddReverseReferences(externalReferences); } /// /// Replaces the generic node with a node specific to the model. /// protected virtual NodeState AddBehaviourToPredefinedNode( ISystemContext context, NodeState predefinedNode) { return predefinedNode; } /// /// Recursively indexes the node and its children. /// protected virtual void AddPredefinedNode(ISystemContext context, NodeState node) { NodeState activeNode = AddBehaviourToPredefinedNode(context, node); PredefinedNodes[activeNode.NodeId] = activeNode; if (activeNode is BaseTypeState type) { AddTypesToTypeTree(type); } var children = new List(); activeNode.GetChildren(context, children); for (int ii = 0; ii < children.Count; ii++) { AddPredefinedNode(context, children[ii]); } } /// /// Recursively indexes the node and its children. /// protected virtual void RemovePredefinedNode( ISystemContext context, NodeState node, List referencesToRemove) { PredefinedNodes.Remove(node.NodeId); node.UpdateChangeMasks(NodeStateChangeMasks.Deleted); node.ClearChangeMasks(context, false); OnNodeRemoved(node); // remove from the parent. if (node is BaseInstanceState instance && instance.Parent != null) { instance.Parent.RemoveChild(instance); } // remove children. var children = new List(); node.GetChildren(context, children); for (int ii = 0; ii < children.Count; ii++) { node.RemoveChild(children[ii]); } for (int ii = 0; ii < children.Count; ii++) { RemovePredefinedNode(context, children[ii], referencesToRemove); } // remove from type table. if (node is BaseTypeState type) { Server.TypeTree.Remove(type.NodeId); } // remove inverse references. var references = new List(); node.GetReferences(context, references); for (int ii = 0; ii < references.Count; ii++) { IReference reference = references[ii]; if (reference.TargetId.IsAbsolute) { continue; } var referenceToRemove = new LocalReference( (NodeId)reference.TargetId, reference.ReferenceTypeId, !reference.IsInverse, node.NodeId); referencesToRemove.Add(referenceToRemove); } } /// /// Called after a node has been deleted. /// protected virtual void OnNodeRemoved(NodeState node) { // overridden by the sub-class. } /// /// Add the node to the set of root notifiers. /// protected virtual void AddRootNotifier(NodeState notifier) { for (int ii = 0; ii < RootNotifiers.Count; ii++) { if (ReferenceEquals(notifier, RootNotifiers[ii])) { return; } } RootNotifiers.Add(notifier); // subscribe to existing events. if (Server.EventManager != null) { IList monitoredItems = Server.EventManager.GetMonitoredItems(); for (int ii = 0; ii < monitoredItems.Count; ii++) { if (monitoredItems[ii].MonitoringAllEvents) { SubscribeToAllEvents(SystemContext, monitoredItems[ii], true, notifier); } } } } /// /// Remove the node from the set of root notifiers. /// protected virtual void RemoveRootNotifier(NodeState? notifier) { for (int ii = 0; ii < RootNotifiers.Count; ii++) { if (ReferenceEquals(notifier, RootNotifiers[ii])) { RootNotifiers.RemoveAt(ii); break; } } } /// /// Ensures that all reverse references exist. /// /// A list of references to add to external targets. protected virtual void AddReverseReferences( IDictionary> externalReferences) { foreach (NodeState source in PredefinedNodes.Values) { // assign a default value to any variable value. if (source is BaseVariableState variable && variable.Value.IsNull) { variable.Value = TypeInfo.GetDefaultVariantValue( variable.DataType, variable.ValueRank, Server.TypeTree); } // add reference from supertype for type nodes. /* BaseTypeState type = source as BaseTypeState; if (type != null && !NodeId.IsNull(type.SuperTypeId)) { if (!IsNodeIdInNamespace(type.SuperTypeId)) { AddExternalReference( type.SuperTypeId, ReferenceTypeIds.HasSubtype, false, type.NodeId, externalReferences); } } */ IList references = []; source.GetReferences(SystemContext, references); for (int ii = 0; ii < references.Count; ii++) { IReference reference = references[ii]; // nothing to do with external nodes. if (reference.TargetId.IsNull || reference.TargetId.IsAbsolute) { continue; } var targetId = (NodeId)reference.TargetId; // add inverse reference to internal targets. if (PredefinedNodes.TryGetValue(targetId, out NodeState? target)) { if (!target.ReferenceExists( reference.ReferenceTypeId, !reference.IsInverse, source.NodeId)) { target.AddReference( reference.ReferenceTypeId, !reference.IsInverse, source.NodeId); } continue; } // check for inverse references to external notifiers. if (reference.IsInverse && reference.ReferenceTypeId == ReferenceTypeIds.HasNotifier) { AddRootNotifier(source); } // nothing more to do for references to nodes managed by this manager. if (IsNodeIdInNamespace(targetId)) { continue; } // add external reference. AddExternalReference( targetId, reference.ReferenceTypeId, !reference.IsInverse, source.NodeId, externalReferences); } } } /// /// Adds an external reference to the dictionary. /// protected void AddExternalReference( NodeId sourceId, NodeId referenceTypeId, bool isInverse, NodeId targetId, IDictionary> externalReferences) { // get list of references to external nodes. if (!externalReferences.TryGetValue(sourceId, out IList? referencesToAdd)) { externalReferences[sourceId] = referencesToAdd = []; } // add reserve reference from external node. var referenceToAdd = new ReferenceNode { ReferenceTypeId = referenceTypeId, IsInverse = isInverse, TargetId = targetId }; referencesToAdd.Add(referenceToAdd); } /// /// Recursively adds the types to the type tree. /// protected void AddTypesToTypeTree(BaseTypeState type) { if (!type.SuperTypeId.IsNull && !Server.TypeTree.IsKnown(type.SuperTypeId)) { AddTypesToTypeTree(type.SuperTypeId); } if (type.NodeClass != NodeClass.ReferenceType) { Server.TypeTree.AddSubtype(type.NodeId, type.SuperTypeId); } else { Server.TypeTree.AddReferenceSubtype(type.NodeId, type.SuperTypeId, type.BrowseName); } } /// /// Recursively adds the types to the type tree. /// protected void AddTypesToTypeTree(NodeId typeId) { if (Find(typeId) is not BaseTypeState type) { return; } AddTypesToTypeTree(type); } /// /// Finds the specified node and checks if it is of the expected type. /// /// Returns null if not found or not of the correct type. [Obsolete("Use FindPredefinedNode instead.")] public NodeState? FindPredefinedNode(NodeId nodeId, Type expectedType) { if (nodeId.IsNull) { return null; } if (!PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return null; } if (expectedType != null && !expectedType.IsInstanceOfType(node)) { return null; } return node; } /// /// Finds the specified node and checks if it is of the expected type. /// /// Node state type /// Returns null if not found or not of the correct type. public T? FindPredefinedNode(NodeId nodeId) where T : NodeState { if (nodeId.IsNull) { return null; } if (!PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return null; } return node is T typedNode ? typedNode : null; } /// /// Searches for a method in the ObjectType hierarchy of an instance node. /// Per OPC UA spec Part 4 section 5.12.2.2, the ObjectType of the Object or a super type /// of that ObjectType may be the source of a HasComponent reference to the method. /// /// The system context. /// The TypeDefinitionId of the object instance. /// The NodeId of the method to find. /// The found method state, or null if not found. private MethodState? FindMethodInTypeHierarchy(ISystemContext context, NodeId typeDefinitionId, NodeId methodId) { // A limit to prevent infinite loops in case of a circular type hierarchy in malformed address spaces. const int maxHierarchyDepth = 100; int depth = 0; NodeId typeId = typeDefinitionId; while (!typeId.IsNull && depth++ < maxHierarchyDepth) { NodeState? typeNode = FindPredefinedNode(typeId); if (typeNode != null) { MethodState? method = typeNode.FindMethod(context, methodId); if (method != null) { return method; } // check for loose coupling via the type node. if (typeNode.ReferenceExists(ReferenceTypeIds.HasComponent, false, methodId)) { method = FindPredefinedNode(methodId); if (method != null) { return method; } } } typeId = Server.TypeTree.FindSuperType(typeId); } return null; } /// /// Resolves a Method declaration found in the ObjectType hierarchy to the /// Method of the Object with the same BrowseName. /// Per OPC UA spec Part 4 section 5.12.2.2 the RolePermissions are always /// verified with the Method that is the target of a HasComponent from the /// Object, independent of the methodId of the call. /// /// The system context. /// The Object the method is called on. /// The Method declaration of the ObjectType. /// The Method of the Object, or the declaration if the Object has none. private MethodState FindObjectMethodForDeclaration( ISystemContext context, NodeState source, MethodState declaration) { if (source.FindChildWithQualifiedName(context, declaration.BrowseName) is MethodState child) { return child; } // check for loose coupling via a HasComponent reference of the Object. var references = new List(); source.GetReferences(context, references, ReferenceTypeIds.HasComponent, false); foreach (IReference reference in references) { if (reference.TargetId.IsNull || reference.TargetId.IsAbsolute) { continue; } MethodState? method = FindPredefinedNode( ExpandedNodeId.ToNodeId(reference.TargetId, Server.NamespaceUris)); if (method != null && method.BrowseName == declaration.BrowseName) { return method; } } return declaration; } /// /// Frees any resources allocated for the address space. /// /// /// Iterates the predefined nodes and invokes on each /// root node so the and /// callbacks fire and /// is reported to subscribers. Nodes that /// are children of another predefined node are skipped because they are deleted /// recursively by their parent (avoiding duplicate callbacks). /// public virtual void DeleteAddressSpace() { lock (Lock) { NodeState[] nodes = [.. PredefinedNodes.Values]; ISystemContext context = SystemContext; foreach (NodeState node in nodes) { if (node is BaseInstanceState instance && instance.Parent != null && !instance.Parent.NodeId.IsNull && PredefinedNodes.ContainsKey(instance.Parent.NodeId)) { continue; } node.Delete(context); } PredefinedNodes.Clear(); } } /// /// Returns a unique handle for the node. /// /// /// This must efficiently determine whether the node belongs to the node manager. If it does belong to /// NodeManager it should return a handle that does not require the NodeId to be validated again when /// the handle is passed into other methods such as 'Read' or 'Write'. /// public virtual object? GetManagerHandle(NodeId nodeId) { lock (Lock) { return GetManagerHandle(SystemContext, nodeId, null); } } /// /// Returns a unique handle for the node. /// /// /// This must efficiently determine whether the node belongs to the node manager. If it does belong to /// NodeManager it should return a handle that does not require the NodeId to be validated again when /// the handle is passed into other methods such as 'Read' or 'Write'. /// protected virtual object? GetManagerHandle( ISystemContext context, NodeId nodeId, IDictionary? cache) { lock (Lock) { // quickly exclude nodes that not in the namespace. if (!IsNodeIdInNamespace(nodeId)) { return null; } // lookup the node. if (!PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return null; } return node; } } /// /// This method is used to add bi-directional references to nodes from other node managers. /// /// /// The additional references are optional, however, the NodeManager should support them. /// public virtual void AddReferences(IDictionary> references) { lock (Lock) { foreach (KeyValuePair> current in references) { // check for valid handle. if (GetManagerHandle(SystemContext, current.Key, null) is not NodeState source) { continue; } // add reference to external target. foreach (IReference reference in current.Value) { source.AddReference( reference.ReferenceTypeId, reference.IsInverse, reference.TargetId); } } } } /// /// This method is used to delete bi-directional references to nodes from other node managers. /// public virtual ServiceResult DeleteReference( object sourceHandle, NodeId referenceTypeId, bool isInverse, ExpandedNodeId targetId, bool deleteBidirectional) { lock (Lock) { // check for valid handle. NodeState? source = IsHandleInNamespace(sourceHandle); if (source == null) { return StatusCodes.BadNodeIdUnknown; } source.RemoveReference(referenceTypeId, isInverse, targetId); if (deleteBidirectional) { // check if the target is also managed by the node manager. if (!targetId.IsAbsolute) { var target = GetManagerHandle( SystemContext, (NodeId)targetId, null) as NodeState; target?.RemoveReference(referenceTypeId, !isInverse, source.NodeId); } } return ServiceResult.Good; } } /// /// Returns the basic metadata for the node. Returns null if the node does not exist. /// /// /// This method validates any placeholder handle. /// public virtual NodeMetadata? GetNodeMetadata( OperationContext context, object targetHandle, BrowseResultMask resultMask) { ServerSystemContext systemContext = SystemContext.Copy(context); lock (Lock) { // check for valid handle. NodeState? target = IsHandleInNamespace(targetHandle); if (target == null) { return null; } // validate node. if (!ValidateNode(systemContext, target)) { return null; } // read the attributes. ArrayOf values = target.ReadAttributes( systemContext, Attributes.WriteMask, Attributes.UserWriteMask, Attributes.DataType, Attributes.ValueRank, Attributes.ArrayDimensions, Attributes.AccessLevel, Attributes.UserAccessLevel, Attributes.EventNotifier, Attributes.Executable, Attributes.UserExecutable); // construct the metadata object. var metadata = new NodeMetadata(target, target.NodeId) { NodeClass = target.NodeClass, BrowseName = target.BrowseName, DisplayName = target.DisplayName }; if (values[0].TryGetValue(out uint writeMask) && values[1].TryGetValue(out uint userWriteMask)) { metadata.WriteMask = (AttributeWriteMask)(writeMask & userWriteMask); } if (values[2].TryGetValue(out NodeId dataType)) { metadata.DataType = dataType; } if (values[3].TryGetValue(out int valueRank)) { metadata.ValueRank = valueRank; } if (values[4].TryGetValue(out ArrayOf arrayDimensions)) { metadata.ArrayDimensions = arrayDimensions; } if (values[5].TryGetValue(out byte accessLevel) && values[6].TryGetValue(out byte userAccessLevel)) { metadata.AccessLevel = (byte)(accessLevel & userAccessLevel); } if (values[7].TryGetValue(out byte eventNotifier)) { metadata.EventNotifier = eventNotifier; } if (values[8].TryGetValue(out bool executable) && values[9].TryGetValue(out bool userExecutable)) { metadata.Executable = executable && userExecutable; } // get instance references. if (target is BaseInstanceState instance) { metadata.TypeDefinition = instance.TypeDefinitionId; metadata.ModellingRule = instance.ModellingRuleId; } // fill in the common attributes. return metadata; } } /// /// Browses the references from a node managed by the node manager. /// /// /// The continuation point is created for every browse operation and contains the browse parameters. /// The node manager can store its state information in the Data and Index properties. /// /// is null. /// public virtual void Browse( OperationContext context, ref ContinuationPoint continuationPoint, IList references) { if (continuationPoint == null) { throw new ArgumentNullException(nameof(continuationPoint)); } if (references == null) { throw new ArgumentNullException(nameof(references)); } // check for view. if (!ViewDescription.IsDefault(continuationPoint.View)) { throw new ServiceResultException(StatusCodes.BadViewIdUnknown); } ServerSystemContext systemContext = SystemContext.Copy(context); lock (Lock) { // verify that the node exists. NodeState source = IsHandleInNamespace(continuationPoint.NodeToBrowse) ?? throw new ServiceResultException(StatusCodes.BadNodeIdUnknown); // validate node. if (!ValidateNode(systemContext, source)) { throw new ServiceResultException(StatusCodes.BadNodeIdUnknown); } // check for previous continuation point. INodeBrowser browser = continuationPoint.Data as INodeBrowser ?? source.CreateBrowser( systemContext, continuationPoint.View, continuationPoint.ReferenceTypeId, continuationPoint.IncludeSubtypes, continuationPoint.BrowseDirection, default, null, false); // apply filters to references. for (IReference? reference = browser.Next(); reference != null; reference = browser.Next()) { // create the type definition reference. ReferenceDescription? description = GetReferenceDescription( context, reference, continuationPoint); if (description == null) { continue; } // check if limit reached. if (continuationPoint.MaxResultsToReturn != 0 && references.Count >= continuationPoint.MaxResultsToReturn) { browser.Push(reference); continuationPoint.Data = browser; return; } references.Add(description); } // release the continuation point if all done. continuationPoint.Dispose(); continuationPoint = null!; } } /// /// Returns the references for the node that meets the criteria specified. /// private ReferenceDescription? GetReferenceDescription( OperationContext context, IReference reference, ContinuationPoint continuationPoint) { // create the type definition reference. var description = new ReferenceDescription { NodeId = reference.TargetId }; description.SetReferenceType( continuationPoint.ResultMask, reference.ReferenceTypeId, !reference.IsInverse); // do not cache target parameters for remote nodes. if (reference.TargetId.IsAbsolute) { // only return remote references if no node class filter is specified. if (continuationPoint.NodeClassMask != 0) { return null; } return description; } NodeState? target = null; // check for local reference. if (reference is NodeStateReference referenceInfo) { target = referenceInfo.Target; } // check for internal reference. if (target == null) { var targetId = (NodeId)reference.TargetId; if (IsNodeIdInNamespace(targetId) && !PredefinedNodes.TryGetValue(targetId, out target)) { target = null; } } // the target may be a reference to a node in another node manager. In these cases // the target attributes must be fetched by the caller. The Unfiltered flag tells the // caller to do that. if (target == null) { description.Unfiltered = true; return description; } // apply node class filter. if (continuationPoint.NodeClassMask != 0 && ((continuationPoint.NodeClassMask & (uint)target.NodeClass) == 0)) { return null; } NodeId typeDefinition = default; if (target is BaseInstanceState instance) { typeDefinition = instance.TypeDefinitionId; } // set target attributes. description.SetTargetAttributes( continuationPoint.ResultMask, target.NodeClass, target.BrowseName, target.DisplayName, typeDefinition); return description; } /// /// Returns the target of the specified browse path fragment(s). /// /// /// If reference exists but the node manager does not know the browse name it must /// return the NodeId as an unresolvedTargetIds. The caller will try to check the /// browse name. /// public virtual void TranslateBrowsePath( OperationContext context, object sourceHandle, RelativePathElement relativePath, IList targetIds, IList unresolvedTargetIds) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); lock (Lock) { // verify that the node exists. NodeState? source = IsHandleInNamespace(sourceHandle); if (source == null) { return; } // validate node. if (!ValidateNode(systemContext, source)) { return; } // get list of references that relative path. INodeBrowser browser = source.CreateBrowser( systemContext, null, relativePath.ReferenceTypeId, relativePath.IncludeSubtypes, relativePath.IsInverse ? BrowseDirection.Inverse : BrowseDirection.Forward, relativePath.TargetName, null, false); // check the browse names. try { for (IReference? reference = browser.Next(); reference != null; reference = browser.Next()) { // ignore unknown external references. if (reference.TargetId.IsAbsolute) { continue; } NodeState? target = null; // check for local reference. if (reference is NodeStateReference referenceInfo) { target = referenceInfo.Target; } if (target == null) { var targetId = (NodeId)reference.TargetId; // the target may be a reference to a node in another node manager. if (!IsNodeIdInNamespace(targetId)) { unresolvedTargetIds.Add((NodeId)reference.TargetId); continue; } // look up the target manually. target = GetManagerHandle( systemContext, targetId, operationCache) as NodeState; if (target == null) { continue; } } // check browse name. if (target.BrowseName == relativePath.TargetName) { // ensure duplicate node ids are not added. if (!targetIds.Contains(reference.TargetId)) { targetIds.Add(reference.TargetId); } } } } finally { browser.Dispose(); } } } /// /// Reads the value for the specified attribute. /// public virtual void Read( OperationContext context, double maxAge, ArrayOf nodesToRead, IList values, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); lock (Lock) { for (int ii = 0; ii < nodesToRead.Count; ii++) { ReadValueId nodeToRead = nodesToRead[ii]; // skip items that have already been processed. if (nodeToRead.Processed) { continue; } // check for valid handle. if (GetManagerHandle( systemContext, nodeToRead.NodeId, operationCache) is not NodeState source) { continue; } // owned by this node manager. nodeToRead.Processed = true; // create an initial value with the current server timestamp. DataValue value = values[ii] = DataValue.Null .WithServerTimestamp(DateTimeUtc.Now); // check if the node is ready for reading. if (source.ValidationRequired) { errors[ii] = StatusCodes.BadNodeIdUnknown; // must validate node in a separate operation. var operation = new ReadWriteOperationState { Source = source, Index = ii }; nodesToValidate.Add(operation); continue; } // read the attribute value. errors[ii] = source.ReadAttribute( systemContext, nodeToRead.AttributeId, nodeToRead.ParsedIndexRange, nodeToRead.DataEncoding, ref value); values[ii] = value; } // check for nothing to do. if (nodesToValidate.Count == 0) { return; } // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { ReadWriteOperationState operation = nodesToValidate[ii]; if (!ValidateNode(systemContext, operation.Source)) { continue; } ReadValueId nodeToRead = nodesToRead[operation.Index]; DataValue value = values[operation.Index]; // update the attribute value. errors[operation.Index] = operation.Source.ReadAttribute( systemContext, nodeToRead.AttributeId, nodeToRead.ParsedIndexRange, nodeToRead.DataEncoding, ref value); } } } /// /// Stores the state of a call method operation. /// private struct ReadWriteOperationState { public NodeState Source; public int Index; } /// /// Verifies that the specified node exists. /// protected virtual bool ValidateNode(ServerSystemContext context, NodeState node) { // validate node only if required. if (node.ValidationRequired) { return node.Validate(context); } return true; } /// /// Reads the history for the specified nodes. /// public virtual void HistoryRead( OperationContext context, HistoryReadDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, ArrayOf nodesToRead, IList results, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); var readsToComplete = new List(); lock (Lock) { for (int ii = 0; ii < nodesToRead.Count; ii++) { HistoryReadValueId nodeToRead = nodesToRead[ii]; // skip items that have already been processed. if (nodeToRead.Processed) { continue; } // check for valid handle. if (GetManagerHandle( systemContext, nodeToRead.NodeId, operationCache) is not NodeState source) { continue; } // owned by this node manager. nodeToRead.Processed = true; // only variables supported. if (source is not BaseVariableState variable) { errors[ii] = StatusCodes.BadHistoryOperationUnsupported; continue; } results[ii] = new HistoryReadResult(); var operation = new ReadWriteOperationState { Source = source, Index = ii }; // check if the node is ready for reading. if (source.ValidationRequired) { // must validate node in a separate operation. errors[ii] = StatusCodes.BadNodeIdUnknown; nodesToValidate.Add(operation); continue; } // read the data. readsToComplete.Add(operation); } // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { ReadWriteOperationState operation = nodesToValidate[ii]; if (!ValidateNode(systemContext, operation.Source)) { continue; } readsToComplete.Add(operation); } } // reads the data without holding onto the lock. for (int ii = 0; ii < readsToComplete.Count; ii++) { ReadWriteOperationState operation = readsToComplete[ii]; errors[operation.Index] = HistoryRead( systemContext, operation.Source, details, timestampsToReturn, releaseContinuationPoints, nodesToRead[operation.Index], results[operation.Index]); } } /// /// Reads the history for a single node which has already been validated. /// protected virtual ServiceResult HistoryRead( ISystemContext context, NodeState source, HistoryReadDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, HistoryReadValueId nodesToRead, HistoryReadResult result) { // check for variable. if (source is not BaseVariableState variable) { return StatusCodes.BadHistoryOperationUnsupported; } // check for access. lock (Lock) { if ((variable.AccessLevel & AccessLevels.HistoryRead) == 0) { return StatusCodes.BadNotReadable; } } // handle read raw. if (details is ReadRawModifiedDetails readRawDetails) { return HistoryReadRaw( context, variable, readRawDetails, timestampsToReturn, releaseContinuationPoints, nodesToRead, result); } // handle read processed. if (details is ReadProcessedDetails readProcessedDetails) { return HistoryReadProcessed( context, variable, readProcessedDetails, timestampsToReturn, releaseContinuationPoints, nodesToRead, result); } // handle read processed. if (details is ReadAtTimeDetails readAtTimeDetails) { return HistoryReadAtTime( context, variable, readAtTimeDetails, timestampsToReturn, releaseContinuationPoints, nodesToRead, result); } return StatusCodes.BadHistoryOperationUnsupported; } /// /// Reads the raw history for the variable value. /// protected virtual ServiceResult HistoryReadRaw( ISystemContext context, BaseVariableState source, ReadRawModifiedDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, HistoryReadValueId nodeToRead, HistoryReadResult result) { return StatusCodes.BadHistoryOperationUnsupported; } /// /// Reads the processed history for the variable value. /// protected virtual ServiceResult HistoryReadProcessed( ISystemContext context, BaseVariableState source, ReadProcessedDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, HistoryReadValueId nodeToRead, HistoryReadResult result) { return StatusCodes.BadHistoryOperationUnsupported; } /// /// Reads the history for the variable value. /// protected virtual ServiceResult HistoryReadAtTime( ISystemContext context, BaseVariableState source, ReadAtTimeDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, HistoryReadValueId nodeToRead, HistoryReadResult result) { return StatusCodes.BadHistoryOperationUnsupported; } /// /// Writes the value for the specified attributes. /// public virtual void Write( OperationContext context, ArrayOf nodesToWrite, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); lock (Lock) { for (int ii = 0; ii < nodesToWrite.Count; ii++) { WriteValue nodeToWrite = nodesToWrite[ii]; // skip items that have already been processed. if (nodeToWrite.Processed) { continue; } // check for valid handle. if (GetManagerHandle( systemContext, nodeToWrite.NodeId, operationCache) is not NodeState source) { continue; } // owned by this node manager. nodeToWrite.Processed = true; // index range is not supported. if (!string.IsNullOrEmpty(nodeToWrite.IndexRange)) { errors[ii] = StatusCodes.BadWriteNotSupported; continue; } // check if the node is ready for reading. if (source.ValidationRequired) { errors[ii] = StatusCodes.BadNodeIdUnknown; // must validate node in a separate operation. var operation = new ReadWriteOperationState { Source = source, Index = ii }; nodesToValidate.Add(operation); continue; } // write the attribute value. errors[ii] = source.WriteAttribute( systemContext, nodeToWrite.AttributeId, nodeToWrite.ParsedIndexRange, nodeToWrite.Value); // updates to source finished - report changes to monitored items. source.ClearChangeMasks(systemContext, false); } // check for nothing to do. if (nodesToValidate.Count == 0) { return; } // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { ReadWriteOperationState operation = nodesToValidate[ii]; if (!ValidateNode(systemContext, operation.Source)) { continue; } WriteValue nodeToWrite = nodesToWrite[operation.Index]; // write the attribute value. errors[operation.Index] = operation.Source.WriteAttribute( systemContext, nodeToWrite.AttributeId, nodeToWrite.ParsedIndexRange, nodeToWrite.Value); // updates to source finished - report changes to monitored items. operation.Source.ClearChangeMasks(systemContext, false); } } } /// /// Updates the history for the specified nodes. /// public virtual void HistoryUpdate( OperationContext context, Type detailsType, ArrayOf nodesToUpdate, IList results, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); lock (Lock) { for (int ii = 0; ii < nodesToUpdate.Count; ii++) { HistoryUpdateDetails nodeToUpdate = nodesToUpdate[ii]; // skip items that have already been processed. if (nodeToUpdate.Processed) { continue; } // check for valid handle. if (GetManagerHandle( systemContext, nodeToUpdate.NodeId, operationCache) is not NodeState source) { continue; } // owned by this node manager. nodeToUpdate.Processed = true; // check if the node is ready for reading. if (source.ValidationRequired) { errors[ii] = StatusCodes.BadNodeIdUnknown; // must validate node in a separate operation. var operation = new ReadWriteOperationState { Source = source, Index = ii }; nodesToValidate.Add(operation); continue; } // historical data not available. errors[ii] = StatusCodes.BadHistoryOperationUnsupported; } // check for nothing to do. if (nodesToValidate.Count == 0) { return; } // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { ReadWriteOperationState operation = nodesToValidate[ii]; if (!ValidateNode(systemContext, operation.Source)) { continue; } // historical data not available. errors[ii] = StatusCodes.BadHistoryOperationUnsupported; } } } /// /// Calls a method on the specified nodes. /// public virtual void Call( OperationContext context, ArrayOf methodsToCall, IList results, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); lock (Lock) { for (int ii = 0; ii < methodsToCall.Count; ii++) { CallMethodRequest methodToCall = methodsToCall[ii]; // skip items that have already been processed. if (methodToCall.Processed) { continue; } // check for valid handle. if (GetManagerHandle( systemContext, methodToCall.ObjectId, operationCache) is not NodeState source) { continue; } // owned by this node manager. methodToCall.Processed = true; // find the method. MethodState? method = source.FindMethod(systemContext, methodToCall.MethodId); if (method == null) { // check for loose coupling. if (source.ReferenceExists( ReferenceTypeIds.HasComponent, false, methodToCall.MethodId)) { method = FindPredefinedNode(methodToCall.MethodId); } // Per OPC UA spec Part 4 section 5.12.2.2: the ObjectType of the Object // or a super type of that ObjectType may also be the source of a HasComponent // reference to the method. if (method == null && source is BaseInstanceState instanceState) { method = FindMethodInTypeHierarchy(systemContext, instanceState.TypeDefinitionId, methodToCall.MethodId); if (method != null) { method = FindObjectMethodForDeclaration(systemContext, source, method); } } if (method == null) { errors[ii] = StatusCodes.BadMethodInvalid; continue; } } CallMethodResult result = results[ii] = new CallMethodResult(); // check if the node is ready for reading. if (source.ValidationRequired) { errors[ii] = StatusCodes.BadNodeIdUnknown; // must validate node in a separate operation. var operation = new CallOperationState { Source = source, Method = method, Index = ii }; nodesToValidate.Add(operation); continue; } // call the method. errors[ii] = Call(systemContext, methodToCall, source, method, result); } // check for nothing to do. if (nodesToValidate.Count == 0) { return; } // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { CallOperationState operation = nodesToValidate[ii]; // validate the object. if (!ValidateNode(systemContext, operation.Source)) { continue; } // call the method. CallMethodResult result = results[operation.Index]; errors[operation.Index] = Call( systemContext, methodsToCall[operation.Index], operation.Source, operation.Method, result); } } } /// /// Stores the state of a call method operation. /// private struct CallOperationState { public NodeState Source; public MethodState Method; public int Index; } /// /// Calls a method on an object. /// protected virtual ServiceResult Call( ISystemContext context, CallMethodRequest methodToCall, NodeState source, MethodState method, CallMethodResult result) { var systemContext = context as ServerSystemContext; var argumentErrors = new List(); var outputArguments = new List(); ServiceResult error = method.Call( context, methodToCall.ObjectId, methodToCall.InputArguments, argumentErrors, outputArguments); if (ServiceResult.IsBad(error)) { return error; } // check for argument errors. bool argumentsValid = true; var inputArgumentResults = new List(); var inputArgumentDiagnosticInfos = new List(); for (int jj = 0; jj < argumentErrors.Count; jj++) { ServiceResult argumentError = argumentErrors[jj]; if (argumentError != null) { inputArgumentResults.Add(argumentError.StatusCode); if (ServiceResult.IsBad(argumentError)) { argumentsValid = false; } // only fill in diagnostic info if it is requested. if ((systemContext!.OperationContext!.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { if (ServiceResult.IsBad(argumentError)) { inputArgumentDiagnosticInfos.Add( new DiagnosticInfo( argumentError, systemContext.OperationContext.DiagnosticsMask, false, systemContext.OperationContext.StringTable, m_logger)); } else { inputArgumentDiagnosticInfos.Add(null!); } } } } // check for validation errors. if (!argumentsValid) { // Per OPC UA Part 4, Section 5.12: InputArgumentResults must // be empty when StatusCode is Good. Therefore add it only in // the error path result.InputArgumentDiagnosticInfos = inputArgumentDiagnosticInfos; result.InputArgumentResults = inputArgumentResults; result.StatusCode = StatusCodes.BadInvalidArgument; return result.StatusCode; } // return output arguments. result.OutputArguments = outputArguments; return ServiceResult.Good; } /// /// Subscribes or unsubscribes to events produced by the specified source. /// /// /// This method is called when a event subscription is created or deletes. The node manager /// must start/stop reporting events for the specified object and all objects below it in /// the notifier hierarchy. /// public virtual ServiceResult SubscribeToEvents( OperationContext context, object sourceId, uint subscriptionId, IEventMonitoredItem monitoredItem, bool unsubscribe) { ServerSystemContext systemContext = SystemContext.Copy(context); lock (Lock) { // check for valid handle. NodeState? source = IsHandleInNamespace(sourceId); if (source == null) { return StatusCodes.BadNodeIdInvalid; } // check if the object supports subscritions. if (sourceId is not BaseObjectState instance || instance.EventNotifier != EventNotifiers.SubscribeToEvents) { return StatusCodes.BadNotSupported; } var monitoredNode = instance.Handle as MonitoredNode; // handle unsubscribe. if (unsubscribe) { if (monitoredNode != null) { monitoredNode.UnsubscribeToEvents(systemContext, monitoredItem); // do any post processing. OnUnsubscribeToEvents(systemContext, monitoredNode, monitoredItem); } return ServiceResult.Good; } // subscribe to events. if (monitoredNode == null) { instance.Handle = monitoredNode = new MonitoredNode(Server, this.ToAsyncNodeManager(), source); } monitoredNode.SubscribeToEvents(systemContext, monitoredItem); // do any post processing. OnSubscribeToEvents(systemContext, monitoredNode, monitoredItem); return ServiceResult.Good; } } /// /// Subscribes or unsubscribes to events produced by all event sources. /// /// /// This method is called when a event subscription is created or deleted. The node /// manager must start/stop reporting events for all objects that it manages. /// public virtual ServiceResult SubscribeToAllEvents( OperationContext context, uint subscriptionId, IEventMonitoredItem monitoredItem, bool unsubscribe) { ServerSystemContext systemContext = SystemContext.Copy(context); lock (Lock) { // update root notifiers. for (int ii = 0; ii < RootNotifiers.Count; ii++) { SubscribeToAllEvents( systemContext, monitoredItem, unsubscribe, RootNotifiers[ii]); } return ServiceResult.Good; } } /// /// Subscribes/unsubscribes to all events produced by the specified node. /// protected void SubscribeToAllEvents( ISystemContext systemContext, IEventMonitoredItem monitoredItem, bool unsubscribe, NodeState source) { var monitoredNode = source.Handle as MonitoredNode; // handle unsubscribe. if (unsubscribe) { if (monitoredNode != null) { monitoredNode.UnsubscribeToEvents(systemContext, monitoredItem); // do any post processing. OnUnsubscribeToEvents(systemContext, monitoredNode, monitoredItem); } return; } // subscribe to events. if (monitoredNode == null) { source.Handle = monitoredNode = new MonitoredNode(Server, this.ToAsyncNodeManager(), source); } monitoredNode.SubscribeToEvents(systemContext, monitoredItem); // do any post processing. OnSubscribeToEvents(systemContext, monitoredNode, monitoredItem); } /// /// Does any processing after a monitored item is subscribed to. /// protected virtual void OnSubscribeToEvents( ISystemContext systemContext, MonitoredNode monitoredNode, IEventMonitoredItem monitoredItem) { // does nothing. } /// /// Does any processing after a monitored item is subscribed to. /// protected virtual void OnUnsubscribeToEvents( ISystemContext systemContext, MonitoredNode monitoredNode, IEventMonitoredItem monitoredItem) { // does nothing. } /// /// Tells the node manager to refresh any conditions associated with the specified monitored items. /// /// /// This method is called when the condition refresh method is called for a subscription. /// The node manager must create a refresh event for each condition monitored by the subscription. /// public virtual ServiceResult ConditionRefresh( OperationContext context, IList monitoredItems) { ServerSystemContext systemContext = SystemContext.Copy(context); lock (Lock) { for (int ii = 0; ii < monitoredItems.Count; ii++) { IEventMonitoredItem monitoredItem = monitoredItems[ii]; if (monitoredItem == null) { continue; } // check for global subscription. if (monitoredItem.MonitoringAllEvents) { for (int jj = 0; jj < RootNotifiers.Count; jj++) { if (RootNotifiers[jj].Handle is not MonitoredNode monitoredNode) { continue; } monitoredNode.ConditionRefresh(systemContext, monitoredItem); } } // check for subscription to local node. else { NodeState? source = IsHandleInNamespace(monitoredItem.ManagerHandle); if (source == null) { continue; } if (source.Handle is not MonitoredNode monitoredNode) { continue; } monitoredNode.ConditionRefresh(systemContext, monitoredItem); } } } return ServiceResult.Good; } /// /// Creates a new set of monitored items for a set of variables. /// /// /// This method only handles data change subscriptions. Event subscriptions are created by the SDK. /// public virtual void CreateMonitoredItems( OperationContext context, uint subscriptionId, double publishingInterval, TimestampsToReturn timestampsToReturn, ArrayOf itemsToCreate, IList errors, IList filterErrors, IList monitoredItems, bool createDurable, MonitoredItemIdFactory monitoredItemIdFactory) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); lock (Lock) { for (int ii = 0; ii < itemsToCreate.Count; ii++) { MonitoredItemCreateRequest itemToCreate = itemsToCreate[ii]; // skip items that have already been processed. if (itemToCreate.Processed) { continue; } ReadValueId itemToMonitor = itemToCreate.ItemToMonitor; // check for valid handle. if (GetManagerHandle( systemContext, itemToMonitor.NodeId, operationCache) is not NodeState source) { continue; } // owned by this node manager. itemToCreate.Processed = true; // check if the node is ready for reading. if (source.ValidationRequired) { errors[ii] = StatusCodes.BadNodeIdUnknown; // must validate node in a separate operation. var operation = new ReadWriteOperationState { Source = source, Index = ii }; nodesToValidate.Add(operation); continue; } errors[ii] = CreateMonitoredItem( systemContext, source, subscriptionId, publishingInterval, context.DiagnosticsMask, timestampsToReturn, itemToCreate, createDurable, monitoredItemIdFactory, out MonitoringFilterResult filterError, out IMonitoredItem monitoredItem); // save any filter error details. filterErrors[ii] = filterError; if (ServiceResult.IsBad(errors[ii])) { monitoredItem?.Dispose(); continue; } // save the monitored item. monitoredItems[ii] = monitoredItem; } // check for nothing to do. if (nodesToValidate.Count == 0) { return; } // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { ReadWriteOperationState operation = nodesToValidate[ii]; // validate the object. if (!ValidateNode(systemContext, operation.Source)) { continue; } MonitoredItemCreateRequest itemToCreate = itemsToCreate[operation.Index]; errors[operation.Index] = CreateMonitoredItem( systemContext, operation.Source, subscriptionId, publishingInterval, context.DiagnosticsMask, timestampsToReturn, itemToCreate, createDurable, monitoredItemIdFactory, out MonitoringFilterResult filterError, out IMonitoredItem monitoredItem); // save any filter error details. filterErrors[operation.Index] = filterError; if (ServiceResult.IsBad(errors[operation.Index])) { monitoredItem?.Dispose(); continue; } // save the monitored item. monitoredItems[operation.Index] = monitoredItem; } } } /// /// Restore a set of monitored items after a restart. /// /// is null. /// public virtual void RestoreMonitoredItems( IList itemsToRestore, IList monitoredItems, IUserIdentity savedOwnerIdentity) { if (itemsToRestore == null) { throw new ArgumentNullException(nameof(itemsToRestore)); } if (monitoredItems == null) { throw new ArgumentNullException(nameof(monitoredItems)); } if (Server.IsRunning) { throw new InvalidOperationException( "Subscription restore can only occur on startup"); } ServerSystemContext systemContext = SystemContext.Copy(); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); lock (Lock) { for (int ii = 0; ii < itemsToRestore.Count; ii++) { IStoredMonitoredItem itemToCreate = itemsToRestore[ii]; // skip items that have already been processed. if (itemToCreate.IsRestored) { continue; } // check for valid handle. if (GetManagerHandle( systemContext, itemToCreate.NodeId, operationCache) is not NodeState source) { continue; } // owned by this node manager. itemToCreate.IsRestored = true; // check if the node is ready for reading. if (source.ValidationRequired) { // must validate node in a separate operation. var operation = new ReadWriteOperationState { Source = source, Index = ii }; nodesToValidate.Add(operation); continue; } bool success = RestoreMonitoredItem( systemContext, source, itemToCreate, out IMonitoredItem monitoredItem); if (!success) { monitoredItem?.Dispose(); continue; } // save the monitored item. monitoredItems[ii] = monitoredItem; } // check for nothing to do. if (nodesToValidate.Count == 0) { return; } // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { ReadWriteOperationState operation = nodesToValidate[ii]; // validate the object. if (!ValidateNode(systemContext, operation.Source)) { continue; } IStoredMonitoredItem itemToCreate = itemsToRestore[operation.Index]; bool success = RestoreMonitoredItem( systemContext, operation.Source, itemToCreate, out IMonitoredItem monitoredItem); if (!success) { monitoredItem?.Dispose(); continue; } // save the monitored item. monitoredItems[operation.Index] = monitoredItem; } } } /// /// Reads the initial value for a monitored item. /// /// The context. /// The monitored node. /// The monitored item. /// If the filters should be ignored. protected virtual ServiceResult ReadInitialValue( ISystemContext context, MonitoredNode node, IDataChangeMonitoredItem2 monitoredItem, bool ignoreFilters) { var initialValue = new DataValue( Variant.Null, StatusCodes.BadWaitingForInitialData, DateTime.MinValue, DateTime.UtcNow); ServiceResult error = node.Node.ReadAttribute( context, monitoredItem.AttributeId, monitoredItem.IndexRange, monitoredItem.DataEncoding, ref initialValue); monitoredItem.QueueValue(initialValue, error, ignoreFilters); return error; } /// /// Validates a data change filter provided by the client. /// /// The system context. /// The node being monitored. /// The attribute being monitored. /// The requested monitoring filter. /// The validated data change filter. /// The EU range associated with the value if required by the filter. /// Any error condition. Good if no errors occurred. protected ServiceResult ValidateDataChangeFilter( ISystemContext context, NodeState source, uint attributeId, ExtensionObject requestedFilter, out DataChangeFilter? filter, out Range? range) { range = null; // check for valid filter type. if (!requestedFilter.TryGetValue(out filter)) { return StatusCodes.BadMonitoredItemFilterUnsupported; } // only supported for value attributes. if (attributeId != Attributes.Value) { return StatusCodes.BadMonitoredItemFilterUnsupported; } // only supported for variables. if (source is not BaseVariableState variable) { return StatusCodes.BadMonitoredItemFilterUnsupported; } // check the datatype. if (filter.DeadbandType != (uint)DeadbandType.None) { BuiltInType builtInType = TypeInfo.GetBuiltInType( variable.DataType, Server.TypeTree); if (!TypeInfo.IsNumericType(builtInType)) { return StatusCodes.BadMonitoredItemFilterUnsupported; } } // validate filter. ServiceResult error = filter.Validate(); if (ServiceResult.IsBad(error)) { return error; } if (filter.DeadbandType == (uint)DeadbandType.Percent) { if (variable.FindChild( context, QualifiedName.From(BrowseNames.EURange)) is not BaseVariableState euRange) { return StatusCodes.BadMonitoredItemFilterUnsupported; } if (!euRange.Value.TryGetStructure(out range!)) { return StatusCodes.BadMonitoredItemFilterUnsupported; } } // all good. return ServiceResult.Good; } /// /// Restore a single monitored Item after a restart /// /// true if sucesfully restored protected virtual bool RestoreMonitoredItem( ServerSystemContext context, NodeState source, IStoredMonitoredItem storedMonitoredItem, out IMonitoredItem monitoredItem) { // create monitored node. if (source.Handle is not MonitoredNode monitoredNode) { source.Handle = monitoredNode = new MonitoredNode(Server, this.ToAsyncNodeManager(), source); } // check if the variable needs to be sampled. bool samplingRequired = false; if (storedMonitoredItem.AttributeId == Attributes.Value) { var variable = source as BaseVariableState; if (variable!.MinimumSamplingInterval > 0) { storedMonitoredItem.SamplingInterval = CalculateSamplingInterval( variable, storedMonitoredItem.SamplingInterval); samplingRequired = true; } } // create the item. DataChangeMonitoredItem datachangeItem = monitoredNode.RestoreDataChangeItem( storedMonitoredItem); if (samplingRequired) { CreateSampledItem(storedMonitoredItem.SamplingInterval, datachangeItem); } // update monitored item list. monitoredItem = datachangeItem; return true; } /// /// Creates a new set of monitored items for a set of variables. /// /// /// This method only handles data change subscriptions. Event subscriptions are created by the SDK. /// protected virtual ServiceResult CreateMonitoredItem( ISystemContext context, NodeState source, uint subscriptionId, double publishingInterval, DiagnosticsMasks diagnosticsMasks, TimestampsToReturn timestampsToReturn, MonitoredItemCreateRequest itemToCreate, bool createDurable, MonitoredItemIdFactory monitoredItemIdFactory, out MonitoringFilterResult filterError, out IMonitoredItem monitoredItem) { filterError = null!; monitoredItem = null!; // read initial value. var initialValue = new DataValue( Variant.Null, StatusCodes.BadWaitingForInitialData, DateTime.MinValue, DateTime.UtcNow); ServiceResult error = source.ReadAttribute( context, itemToCreate.ItemToMonitor.AttributeId, itemToCreate.ItemToMonitor.ParsedIndexRange, itemToCreate.ItemToMonitor.DataEncoding, ref initialValue); if (ServiceResult.IsBad(error)) { if (error.StatusCode == StatusCodes.BadAttributeIdInvalid || error.StatusCode == StatusCodes.BadDataEncodingInvalid || error.StatusCode == StatusCodes.BadDataEncodingUnsupported) { return error; } initialValue = initialValue.WithStatus(error.StatusCode); _ = ServiceResult.Good; } // validate parameters. MonitoringParameters parameters = itemToCreate.RequestedParameters; // validate the data change filter. DataChangeFilter? filter = null; Range? range = null; if (!parameters.Filter.IsNull) { error = ValidateDataChangeFilter( context, source, itemToCreate.ItemToMonitor.AttributeId, parameters.Filter, out filter, out range); if (ServiceResult.IsBad(error)) { return error; } } // create monitored node. if (source.Handle is not MonitoredNode monitoredNode) { source.Handle = monitoredNode = new MonitoredNode(Server, this.ToAsyncNodeManager(), source); } // determine the sampling interval. double samplingInterval = itemToCreate.RequestedParameters.SamplingInterval; if (samplingInterval < 0) { samplingInterval = publishingInterval; } // check if the variable needs to be sampled. bool samplingRequired = false; if (itemToCreate.ItemToMonitor.AttributeId == Attributes.Value) { var variable = source as BaseVariableState; if (variable!.MinimumSamplingInterval > 0) { samplingInterval = CalculateSamplingInterval(variable, samplingInterval); samplingRequired = true; } } // create the item. DataChangeMonitoredItem datachangeItem = monitoredNode.CreateDataChangeItem( context, monitoredItemIdFactory.GetNextId(), itemToCreate.ItemToMonitor.AttributeId, itemToCreate.ItemToMonitor.ParsedIndexRange, itemToCreate.ItemToMonitor.DataEncoding, diagnosticsMasks, timestampsToReturn, itemToCreate.MonitoringMode, itemToCreate.RequestedParameters.ClientHandle, samplingInterval, itemToCreate.RequestedParameters.QueueSize, itemToCreate.RequestedParameters.DiscardOldest, filter, range, false); if (samplingRequired) { CreateSampledItem(samplingInterval, datachangeItem); } // report the initial value. datachangeItem.QueueValue(initialValue, null, true); // do any post processing. OnCreateMonitoredItem(context, itemToCreate, monitoredNode, datachangeItem); // update monitored item list. monitoredItem = datachangeItem; return ServiceResult.Good; } /// /// Calculates the sampling interval. /// private double CalculateSamplingInterval( BaseVariableState variable, double samplingInterval) { if (samplingInterval < variable.MinimumSamplingInterval) { samplingInterval = variable.MinimumSamplingInterval; } if ((samplingInterval % m_minimumSamplingInterval) != 0) { samplingInterval = Math.Truncate(samplingInterval / m_minimumSamplingInterval); samplingInterval++; samplingInterval *= m_minimumSamplingInterval; } return samplingInterval; } /// /// Creates a new sampled item. /// private void CreateSampledItem( double samplingInterval, DataChangeMonitoredItem monitoredItem) { m_sampledItems.Add(monitoredItem); TimeProvider timeProvider = (Server as ITimeProviderProvider)?.TimeProvider ?? TimeProvider.System; m_samplingTimer ??= timeProvider.CreateTimer( DoSample, null, TimeSpan.FromMilliseconds(m_minimumSamplingInterval), TimeSpan.FromMilliseconds(m_minimumSamplingInterval)); } /// /// Deletes a sampled item. /// private void DeleteSampledItem(DataChangeMonitoredItem monitoredItem) { for (int ii = 0; ii < m_sampledItems.Count; ii++) { if (ReferenceEquals(monitoredItem, m_sampledItems[ii])) { m_sampledItems.RemoveAt(ii); break; } } if (m_sampledItems.Count == 0 && m_samplingTimer != null) { m_samplingTimer.Dispose(); m_samplingTimer = null; } } /// /// Polls each monitored item which requires sample. /// private void DoSample(object? state) { try { lock (Lock) { for (int ii = 0; ii < m_sampledItems.Count; ii++) { DataChangeMonitoredItem monitoredItem = m_sampledItems[ii]; if (monitoredItem.TimeToNextSample < m_minimumSamplingInterval) { monitoredItem.ValueChanged(SystemContext); } } } } catch (Exception e) { m_logger.UnexpectedErrorDuringDiagnosticsScan(e); } } /// /// Does any processing after a monitored item is created. /// protected virtual void OnCreateMonitoredItem( ISystemContext systemContext, MonitoredItemCreateRequest itemToCreate, MonitoredNode monitoredNode, DataChangeMonitoredItem monitoredItem) { // does nothing. } /// /// Modifies the parameters for a set of monitored items. /// public virtual void ModifyMonitoredItems( OperationContext context, TimestampsToReturn timestampsToReturn, IList monitoredItems, ArrayOf itemsToModify, IList errors, IList filterErrors) { ServerSystemContext systemContext = SystemContext.Copy(context); lock (Lock) { for (int ii = 0; ii < monitoredItems.Count; ii++) { MonitoredItemModifyRequest itemToModify = itemsToModify[ii]; // skip items that have already been processed. if (itemToModify.Processed) { continue; } // modify the monitored item. errors[ii] = ModifyMonitoredItem( systemContext, context.DiagnosticsMask, timestampsToReturn, monitoredItems[ii], itemToModify, out MonitoringFilterResult filterError); // save any filter error details. filterErrors[ii] = filterError; } } } /// /// Modifies the parameters for a monitored item. /// protected virtual ServiceResult ModifyMonitoredItem( ISystemContext context, DiagnosticsMasks diagnosticsMasks, TimestampsToReturn timestampsToReturn, IMonitoredItem monitoredItem, MonitoredItemModifyRequest itemToModify, out MonitoringFilterResult filterError) { filterError = null!; // check for valid handle. if (monitoredItem.ManagerHandle is not MonitoredNode monitoredNode) { return ServiceResult.Good; } if (IsHandleInNamespace(monitoredNode.Node) == null) { return ServiceResult.Good; } // owned by this node manager. itemToModify.Processed = true; // check for valid monitored item. var datachangeItem = monitoredItem as DataChangeMonitoredItem; // validate parameters. MonitoringParameters parameters = itemToModify.RequestedParameters; // validate the data change filter. DataChangeFilter? filter = null; Range? range = null; ServiceResult error; if (!parameters.Filter.IsNull) { error = ValidateDataChangeFilter( context, monitoredNode.Node, datachangeItem!.AttributeId, parameters.Filter, out filter, out range); if (ServiceResult.IsBad(error)) { return error; } } double previousSamplingInterval = datachangeItem!.SamplingInterval; // check if the variable needs to be sampled. double samplingInterval = itemToModify.RequestedParameters.SamplingInterval; if (datachangeItem.AttributeId == Attributes.Value) { var variable = monitoredNode.Node as BaseVariableState; if (variable!.MinimumSamplingInterval > 0) { samplingInterval = CalculateSamplingInterval(variable, samplingInterval); } } // modify the monitored item parameters. _ = datachangeItem.Modify( diagnosticsMasks, timestampsToReturn, itemToModify.RequestedParameters.ClientHandle, samplingInterval, itemToModify.RequestedParameters.QueueSize, itemToModify.RequestedParameters.DiscardOldest, filter, range); // do any post processing. OnModifyMonitoredItem( context, itemToModify, monitoredNode, datachangeItem, previousSamplingInterval); return ServiceResult.Good; } /// /// Does any processing after a monitored item is created. /// protected virtual void OnModifyMonitoredItem( ISystemContext systemContext, MonitoredItemModifyRequest itemToModify, MonitoredNode monitoredNode, DataChangeMonitoredItem monitoredItem, double previousSamplingInterval) { // does nothing. } /// /// Deletes a set of monitored items. /// public virtual void DeleteMonitoredItems( OperationContext context, IList monitoredItems, IList processedItems, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); lock (Lock) { for (int ii = 0; ii < monitoredItems.Count; ii++) { // skip items that have already been processed. if (processedItems[ii]) { continue; } // delete the monitored item. errors[ii] = DeleteMonitoredItem( systemContext, monitoredItems[ii], out bool processed); // indicate whether it was processed or not. processedItems[ii] = processed; } } } /// /// Deletes a monitored item. /// protected virtual ServiceResult DeleteMonitoredItem( ISystemContext context, IMonitoredItem monitoredItem, out bool processed) { processed = false; // check for valid handle. if (monitoredItem.ManagerHandle is not MonitoredNode monitoredNode) { return ServiceResult.Good; } if (IsHandleInNamespace(monitoredNode.Node) == null) { return ServiceResult.Good; } // owned by this node manager. processed = true; // check for valid monitored item. var datachangeItem = monitoredItem as DataChangeMonitoredItem; // check if the variable needs to be sampled. if (datachangeItem!.AttributeId == Attributes.Value) { var variable = monitoredNode.Node as BaseVariableState; if (variable!.MinimumSamplingInterval > 0) { DeleteSampledItem(datachangeItem); } } // remove item. monitoredNode.DeleteItem(datachangeItem); // do any post processing. OnDeleteMonitoredItem(context, monitoredNode, datachangeItem); return ServiceResult.Good; } /// /// Does any processing after a monitored item is deleted. /// protected virtual void OnDeleteMonitoredItem( ISystemContext systemContext, MonitoredNode monitoredNode, DataChangeMonitoredItem monitoredItem) { // does nothing. } /// /// Transfers a set of monitored items. /// /// The context. /// Whether the subscription should send initial values after transfer. /// The set of monitoring items to update. /// The list of bool with items that were already processed. /// Any errors. /// Options that describe the transfer execution. public virtual void TransferMonitoredItems( OperationContext context, bool sendInitialValues, IList monitoredItems, IList processedItems, IList errors, MonitoredItemTransferOptions transferOptions) { ServerSystemContext systemContext = SystemContext.Copy(context); IList transferredItems = []; bool deferInitialValues = transferOptions.DeferInitialValues; lock (Lock) { for (int ii = 0; ii < monitoredItems.Count; ii++) { // skip items that have already been processed. if (processedItems[ii] || monitoredItems[ii] == null) { continue; } // check handle. // check for valid handle. if (monitoredItems[ii].ManagerHandle is not MonitoredNode monitoredNode) { continue; } // owned by this node manager. processedItems[ii] = true; transferredItems.Add(monitoredItems[ii]); if (sendInitialValues && !deferInitialValues) { monitoredItems[ii].SetupResendDataTrigger(); } errors[ii] = StatusCodes.Good; } } // do any post processing. OnMonitoredItemsTransferred(systemContext, transferredItems); } /// /// Called after transfer of MonitoredItems. /// /// The context. /// The transferred monitored items. protected virtual void OnMonitoredItemsTransferred( ServerSystemContext context, IList monitoredItems) { // overridden by the sub-class. } /// /// Changes the monitoring mode for a set of monitored items. /// public virtual void SetMonitoringMode( OperationContext context, MonitoringMode monitoringMode, IList monitoredItems, IList processedItems, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); lock (Lock) { for (int ii = 0; ii < monitoredItems.Count; ii++) { // skip items that have already been processed. if (processedItems[ii]) { continue; } // update monitoring mode. errors[ii] = SetMonitoringMode( systemContext, monitoredItems[ii], monitoringMode, out bool processed); // indicate whether it was processed or not. processedItems[ii] = processed; } } } /// /// Changes the monitoring mode for an item. /// protected virtual ServiceResult SetMonitoringMode( ISystemContext context, IMonitoredItem monitoredItem, MonitoringMode monitoringMode, out bool processed) { processed = false; // check for valid handle. if (monitoredItem.ManagerHandle is not MonitoredNode monitoredNode) { return ServiceResult.Good; } if (IsHandleInNamespace(monitoredNode.Node) == null) { return ServiceResult.Good; } // owned by this node manager. processed = true; // check for valid monitored item. var datachangeItem = monitoredItem as DataChangeMonitoredItem; // update monitoring mode. MonitoringMode previousMode = datachangeItem!.SetMonitoringMode(monitoringMode); // need to provide an immediate update after enabling. if (previousMode == MonitoringMode.Disabled && monitoringMode != MonitoringMode.Disabled) { ReadInitialValue(context, monitoredNode, datachangeItem, false); } // do any post processing. OnSetMonitoringMode( context, monitoredNode, datachangeItem, previousMode, monitoringMode); return ServiceResult.Good; } /// /// Does any processing after a monitored item is created. /// protected virtual void OnSetMonitoringMode( ISystemContext systemContext, MonitoredNode monitoredNode, DataChangeMonitoredItem monitoredItem, MonitoringMode previousMode, MonitoringMode currentMode) { // does nothing. } private IList m_namespaceUris = null!; private ushort[] m_namespaceIndexes = null!; private ITimer? m_samplingTimer; private readonly ILogger m_logger; private readonly List m_sampledItems = []; private readonly double m_minimumSamplingInterval; } internal static partial class SampleNodeManagerLog { [LoggerMessage( EventId = QuickstartsServersEventIds.SampleNodeManager + 0, Level = LogLevel.Error, Message = "Unexpected error during diagnostics scan.")] public static partial void UnexpectedErrorDuringDiagnosticsScan(this ILogger logger, Exception exception); } }