/* ======================================================================== * 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.Globalization; using Opc.Ua; using Opc.Ua.Sample; using Opc.Ua.Server; namespace MemoryBuffer { /// /// The factory to create the node manager for memory buffers. /// public class MemoryBufferNodeManagerFactory : INodeManagerFactory { /// public INodeManager Create(IServerInternal server, ApplicationConfiguration configuration) { return new MemoryBufferNodeManager(server, configuration, NamespacesUris.ToArray()!); } /// public ArrayOf NamespacesUris => [Namespaces.MemoryBuffer, Namespaces.MemoryBuffer + "/Instance"]; } /// /// A node manager for a variety of memory buffers. /// public class MemoryBufferNodeManager : SampleNodeManager { /// /// Initializes the node manager. /// public MemoryBufferNodeManager( IServerInternal server, ApplicationConfiguration configuration, string[] namespaceUris) : base(server) { NamespaceUris = namespaceUris; // get the configuration for the node manager. m_configuration = configuration.ParseExtension() ?? new MemoryBufferConfiguration(); // use suitable defaults if no configuration exists. m_buffers = []; } /// /// 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 override void CreateAddressSpace( IDictionary> externalReferences) { lock (Lock) { base.CreateAddressSpace(externalReferences); // create the nodes from configuration. ushort namespaceIndex = Server.NamespaceUris .GetIndexOrAppend(Namespaces.MemoryBuffer); BaseInstanceState root = FindPredefinedNode( new NodeId(Objects.MemoryBuffers, namespaceIndex))!; // create the nodes from configuration. namespaceIndex = Server.NamespaceUris .GetIndexOrAppend(Namespaces.MemoryBuffer + "/Instance"); // the buffers live in the instance namespace, so their // children are minted there too rather than in the model // namespace the base class would otherwise have adopted. NodeIdFactory = NodeIdFactory.WithDefaultNamespaceIndex(namespaceIndex); if (m_configuration != null && !m_configuration.Buffers.IsNull) { for (int ii = 0; ii < m_configuration.Buffers.Count; ii++) { MemoryBufferInstance instance = m_configuration.Buffers[ii]!; // create a new buffer. var bufferNode = new MemoryBufferState(SystemContext, instance); // assign node ids. bufferNode.Create( SystemContext, new NodeId(bufferNode.SymbolicName, namespaceIndex), new QualifiedName(bufferNode.SymbolicName, namespaceIndex), default, true); bufferNode.CreateBuffer(instance.DataType, instance.TagCount); bufferNode.InitializeMonitoring(Server, this); // link to root. root.AddChild(bufferNode); // Register the buffer subtree. Its own properties - // StartAddress and SizeInBytes - are real nodes with // real identifiers, and a client that browses the // buffer is handed them; without this they resolve to // nothing. The virtual tags below are still ephemeral // and are not registered. AddPredefinedNode(SystemContext, bufferNode); // save the buffers for easy look up later. m_buffers[bufferNode.SymbolicName] = bufferNode; } } } } /// /// Loads a node set from a file or resource and adds them to the set of predefined nodes. /// protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context) { return new NodeStateCollection().AddMemoryBuffer(context); } /// /// Frees any resources allocated for the address space. /// public override void DeleteAddressSpace() { lock (Lock) { base.DeleteAddressSpace(); } } /// /// 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 override object? GetManagerHandle( ISystemContext context, NodeId nodeId, IDictionary? cache) { lock (Lock) { if (!IsNodeIdInNamespace(nodeId)) { return null; } if (nodeId.TryGetValue(out string? id)) { // check for a reference to the buffer. if (m_buffers.TryGetValue(id!, out MemoryBufferState? buffer)) { return buffer; } // Tag ids have the syntax [
]. An id // in any other shape is an ordinary registered node - the // buffer's own properties carry identifiers minted by the // manager's factory - so it falls through to the base // lookup rather than being reported as unknown. MemoryTagState? tag = ResolveVirtualTag(id!); if (tag != null) { return tag; } } return base.GetManagerHandle(context, nodeId, cache); } } /// /// Resolves an identifier of the form <bufferName>[<address>] /// to a tag inside that buffer. /// /// /// The tags carry all the metadata the UA operations need plus /// pointers into the buffer object, and are discarded once the /// operation completes. That is what lets the server expose millions /// of nodes without holding millions of objects in memory. /// /// The string identifier to parse. /// The tag, or null when the id is not a tag id. private MemoryTagState? ResolveVirtualTag(string id) { if (id.Length == 0 || id[^1] != ']') { return null; } int index = id.IndexOf('[', StringComparison.Ordinal); if (index == -1) { return null; } if (!m_buffers.TryGetValue(id[..index], out MemoryBufferState? buffer)) { return null; } string offsetText = id.Substring(index + 1, id.Length - index - 2); for (int ii = 0; ii < offsetText.Length; ii++) { if (!char.IsDigit(offsetText[ii])) { return null; } } if (offsetText.Length == 0) { return null; } uint offset = Convert.ToUInt32(offsetText, CultureInfo.InvariantCulture); if (offset >= buffer.SizeInBytes!.Value) { return null; } return new MemoryTagState(buffer, offset); } /// /// 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 override 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!; // use default behavior for non-tag sources. if (source is not MemoryTagState tag) { return base.CreateMonitoredItem( context, source, subscriptionId, publishingInterval, diagnosticsMasks, timestampsToReturn, itemToCreate, createDurable, monitoredItemIdFactory, out filterError, out monitoredItem); } // validate parameters. MonitoringParameters parameters = itemToCreate.RequestedParameters; // no filters supported at this time. var filter = (MonitoringFilter?)ExtensionObject.ToEncodeable(parameters.Filter); if (filter != null) { return StatusCodes.BadFilterNotAllowed; } // index range not supported. if (!itemToCreate.ItemToMonitor.ParsedIndexRange.IsNull) { return StatusCodes.BadIndexRangeInvalid; } // data encoding not supported. if (!itemToCreate.ItemToMonitor.DataEncoding.IsNull) { return StatusCodes.BadDataEncodingUnsupported; } // read initial value. var initialValue = new DataValue( Variant.Null, StatusCodes.Good, DateTime.MinValue, DateTime.UtcNow); ServiceResult error = source.ReadAttribute( context, itemToCreate.ItemToMonitor.AttributeId, itemToCreate.ItemToMonitor.ParsedIndexRange, itemToCreate.ItemToMonitor.DataEncoding, ref initialValue); if (ServiceResult.IsBad(error)) { return error; } // get the monitored node for the containing buffer. if (tag.Parent is not MemoryBufferState buffer) { return StatusCodes.BadInternalError; } // determine the sampling interval. double samplingInterval = itemToCreate.RequestedParameters.SamplingInterval; if (samplingInterval < 0) { samplingInterval = publishingInterval; } // create the item. MemoryBufferMonitoredItem datachangeItem = buffer.CreateDataChangeItem( (ServerSystemContext)context, tag, subscriptionId, monitoredItemIdFactory.GetNextId(), itemToCreate.ItemToMonitor, diagnosticsMasks, timestampsToReturn, itemToCreate.MonitoringMode, itemToCreate.RequestedParameters.ClientHandle, samplingInterval, createDurable); // report the initial value. datachangeItem.QueueValue(initialValue, null); // update monitored item list. monitoredItem = datachangeItem; return ServiceResult.Good; } /// /// Restore a single monitored Item after a restart /// /// true if sucesfully restored protected override bool RestoreMonitoredItem( ServerSystemContext context, NodeState source, IStoredMonitoredItem storedMonitoredItem, out IMonitoredItem monitoredItem) { monitoredItem = null!; // use default behavior for non-tag sources. if (source is not MemoryTagState tag) { return base.RestoreMonitoredItem( context, source, storedMonitoredItem, out monitoredItem); } // get the monitored node for the containing buffer. if (tag.Parent is not MemoryBufferState buffer) { return false; } // create the item. // update monitored item list. monitoredItem = buffer.RestoreDataChangeItem(context, tag, storedMonitoredItem); return true; } /// /// Modifies the parameters for a monitored item. /// protected override 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 MemoryBufferState) { return base.ModifyMonitoredItem( context, diagnosticsMasks, timestampsToReturn, monitoredItem, itemToModify, out filterError); } // owned by this node manager. itemToModify.Processed = true; // get the monitored item. if (monitoredItem is not MemoryBufferMonitoredItem datachangeItem) { return StatusCodes.BadMonitoredItemIdInvalid; } // validate parameters. MonitoringParameters parameters = itemToModify.RequestedParameters; // no filters supported at this time. var filter = (MonitoringFilter?)ExtensionObject.ToEncodeable(parameters.Filter); if (filter != null) { return StatusCodes.BadFilterNotAllowed; } // modify the monitored item parameters. _ = datachangeItem.Modify( diagnosticsMasks, timestampsToReturn, itemToModify.RequestedParameters.ClientHandle, itemToModify.RequestedParameters.SamplingInterval); return ServiceResult.Good; } /// /// Deletes a monitored item. /// protected override ServiceResult DeleteMonitoredItem( ISystemContext context, IMonitoredItem monitoredItem, out bool processed) { // check for valid handle. if (monitoredItem.ManagerHandle is not MemoryBufferState buffer) { return base.DeleteMonitoredItem(context, monitoredItem, out processed); } // owned by this node manager. processed = true; // get the monitored item. if (monitoredItem is not MemoryBufferMonitoredItem datachangeItem) { return StatusCodes.BadMonitoredItemIdInvalid; } // delete the item. buffer.DeleteItem(datachangeItem); return ServiceResult.Good; } /// /// Changes the monitoring mode for an item. /// protected override ServiceResult SetMonitoringMode( ISystemContext context, IMonitoredItem monitoredItem, MonitoringMode monitoringMode, out bool processed) { // check for valid handle. if (monitoredItem.ManagerHandle is not MemoryBufferState buffer) { return base.SetMonitoringMode( context, monitoredItem, monitoringMode, out processed); } // owned by this node manager. processed = true; // get the monitored item. if (monitoredItem is not MemoryBufferMonitoredItem datachangeItem) { return StatusCodes.BadMonitoredItemIdInvalid; } // delete the item. MonitoringMode previousMode = datachangeItem.SetMonitoringMode(monitoringMode); // need to provide an immediate update after enabling. if (previousMode == MonitoringMode.Disabled && monitoringMode != MonitoringMode.Disabled) { var initialValue = new DataValue( Variant.Null, StatusCodes.Good, DateTime.MinValue, DateTime.UtcNow); var tag = new MemoryTagState(buffer, datachangeItem.Offset); ServiceResult error = tag.ReadAttribute( context, datachangeItem.AttributeId, default, default, ref initialValue); datachangeItem.QueueValue(initialValue, error); } return ServiceResult.Good; } private readonly MemoryBufferConfiguration m_configuration; private readonly Dictionary m_buffers; } }